TypeScript Strict Mode and tsconfig Tips
Understand what strict mode actually turns on, which extra flags are worth enabling, and how to adopt strictness incrementally.
What you'll learn
- ✓What strict enables
- ✓Useful extra flags
- ✓Path aliases
- ✓Module resolution
- ✓Incremental adoption
Prerequisites
- •Comfortable with JS
What and Why
"strict": true in tsconfig.json is a meta-flag that turns on a family of safety checks. Each one closes a class of bugs: implicit any, unchecked null, unsound function parameters, uninitialized class fields. Combined, they push your code toward the guarantees that make TypeScript worth using in the first place.
If you skip strict mode, you are paying TypeScript’s costs (build step, learning curve) without collecting the benefits. The goal of this article is to demystify what strict actually does and which neighbouring flags reward the effort.
Mental Model
Think of TypeScript’s checks as a dial. At the loosest end, the compiler trusts you and emits anything. At the strictest end, every uncertainty is a compile error. Strict mode parks the dial at a pragmatic level: strict enough to be useful, loose enough that most real code compiles after a cleanup pass.
loose --------------------------------- strict
| |
noImplicitAny ---- strictNullChecks ---- noUncheckedIndexedAccess
|
strictFunctionTypes strictPropertyInitialization Hands-on Example
A reasonable starting tsconfig.json for a modern project:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": { "@/*": ["src/*"] }
},
"include": ["src"]
}
noUncheckedIndexedAccess makes arr[i] return T | undefined, forcing you to handle holes:
const first = items[0];
if (first) first.toUpperCase();
exactOptionalPropertyTypes distinguishes { x?: string } from { x: string | undefined }, which matters when a property’s presence is meaningful.
For libraries, add declaration: true and declarationMap: true so consumers get autocomplete and jump-to-source. For apps, set noEmit: true and let your bundler handle output.
Common Pitfalls
A few sharp edges to watch for:
- skipLibCheck off. Without it, broken type packages from
node_modulescan block your build for no benefit. - Mismatched
moduleandmoduleResolution. Modern bundlers expect"bundler"; Node CJS expects"node16"or"nodenext". Pick the pair that matches your runtime. - Path aliases that the bundler does not know about. TypeScript resolves
@/foo, but Webpack or Vite need separate config. anyleaking through casts. Strict mode does not stop you from writingas any. Linters can flag this.- Turning strict on all at once on a legacy codebase. You will drown. Enable one flag at a time.
Best Practices
Adopt strict mode incrementally. Start with noImplicitAny, then strictNullChecks (the biggest win and the biggest workload), then the rest. Each PR should turn on one flag and fix the resulting errors so reviewers can follow.
Keep two configs if needed: a strict tsconfig.json for new code and a tsconfig.legacy.json that loosens specific files via include. Aim to delete the legacy file over time.
Enable noUncheckedIndexedAccess once your codebase is null-safe; it catches subtle holes in maps and arrays. Add verbatimModuleSyntax to keep import elision predictable. Pair TypeScript with ESLint rules like @typescript-eslint/no-explicit-any and no-floating-promises to cover gaps the type system does not.
Finally, run tsc --noEmit in CI. The IDE can drift from the command-line compiler, especially around project references; CI is the source of truth.
Wrap-up
Strict mode is the floor, not the ceiling. Turn it on, add a couple of complementary flags, and let the compiler do its job. The first cleanup hurts; everything after is faster, safer, and easier to refactor.
Related articles
- Node.js Node.js TypeScript Setup Tutorial
Set up a clean, modern TypeScript project for Node.js with tsconfig, build scripts, ESM, and a dev loop that does not get in your way.
- TypeScript TypeScript Branded Types Tutorial
Use branded types in TypeScript to add nominal safety on top of structural types. Stop mixing up UserIds, emails, and raw strings at compile time.
- TypeScript TypeScript Conditional Types Tutorial
Learn conditional types in TypeScript: distribution, infer, and how to build expressive utility types that adapt to the input shape.
- TypeScript TypeScript Discriminated Unions
How to model variant types in TypeScript with discriminated unions: design, narrowing, exhaustiveness checks, and real-world patterns.