TypeScript — Strict Mode & Migration
strict: true is a bundle of soundness flags. New projects should start strict. Existing JS codebases should migrate flag-by-flag with measurable progress — not a big-bang rewrite.
| Flag | Catches | Typical fix |
|---|---|---|
| noImplicitAny | Missing annotations becoming any | Annotate or fix inference |
| strictNullChecks | null/undefined not in domain types | Optional chaining, unions, guards |
| strictFunctionTypes | Unsafe callback variance | Correct parameter types |
| strictBindCallApply | Wrong bind/call/apply args | Fix arguments |
| strictPropertyInitialization | Class fields unset | Initializer, definite assignment, constructor |
| noImplicitThis | this: any | Arrow functions or annotate this |
| useUnknownInCatchVariables | catch (e) is unknown | Narrow e |
| alwaysStrict | Emit 'use strict' | Usually fine |
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "strict": true, |
| 4 | "noUncheckedIndexedAccess": true, |
| 5 | "exactOptionalPropertyTypes": true, |
| 6 | "noImplicitOverride": true, |
| 7 | "noFallthroughCasesInSwitch": true, |
| 8 | "noPropertyAccessFromIndexSignature": true |
| 9 | } |
| 10 | } |
pro tip
Recommended order for migrations:
1. allowJs + checkJs (optional) → see errors in JS
2. noImplicitAny
3. strictNullChecks (biggest value, biggest churn)
4. Remaining strict bundle flags
5. noUncheckedIndexedAccess
6. exactOptionalPropertyTypes (stricter optional semantics)
| 1 | // Before strictNullChecks |
| 2 | function len(s: string) { |
| 3 | return s.length; |
| 4 | } |
| 5 | len(null); // runtime boom — allowed without strictNullChecks |
| 6 | |
| 7 | // After |
| 8 | function len2(s: string | null) { |
| 9 | return s?.length ?? 0; |
| 10 | } |
Phase 0 — Tooling
| 1 | npm install -D typescript @types/node |
| 2 | npx tsc --init |
| 3 | # rename incrementally: .js → .ts / .jsx → .tsx |
Phase 1 — Coexistence
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "allowJs": true, |
| 4 | "checkJs": false, |
| 5 | "outDir": "dist", |
| 6 | "noEmit": true |
| 7 | }, |
| 8 | "include": ["src"] |
| 9 | } |
Phase 2 — Rename & annotate
Rename leaf modules first (utils with few imports). Add JSDoc types in JS if rename must wait:
| 1 | /** @param {string} id @returns {Promise<import('./types').User>} */ |
| 2 | export async function loadUser(id) { /* ... */ } |
Phase 3 — Strictness ratchets
Track error count per flag. Use // @ts-expect-error sparingly with tickets. Ban @ts-ignore.
best practice
| Error pattern | Fix |
|---|---|
| Object is possibly undefined | Optional chain, early return, or ! only if proven |
| Implicit any parameter | Annotate callback params |
| Index signature undefined | Check before use; default |
| Property missing in class | Initialize or use definite assignment assertion carefully |
| JSON.parse returns any | Type as unknown + Zod |
| 1 | const map = new Map<string, number>(); |
| 2 | const n = map.get("x"); // number | undefined |
| 3 | if (n !== undefined) { |
| 4 | console.log(n.toFixed(2)); |
| 5 | } |
| 6 | |
| 7 | type Dict = Record<string, number>; |
| 8 | function read(d: Dict, k: string) { |
| 9 | const v = d[k]; // number | undefined with noUncheckedIndexedAccess |
| 10 | return v ?? 0; |
| 11 | } |
| 1 | type Opts = { debug?: boolean }; |
| 2 | |
| 3 | // With exactOptionalPropertyTypes: |
| 4 | const a: Opts = { debug: true }; // OK |
| 5 | const b: Opts = { debug: undefined }; // Error — undefined ≠ missing |
| 6 | const c: Opts = {}; // OK |
| 1 | { |
| 2 | "rules": { |
| 3 | "@typescript-eslint/no-explicit-any": "error", |
| 4 | "@typescript-eslint/no-non-null-assertion": "warn", |
| 5 | "@typescript-eslint/consistent-type-imports": "error" |
| 6 | } |
| 7 | } |
| Step | Done when |
|---|---|
| strict: true in base tsconfig | All packages extend it |
| Zero any in public APIs | eslint forbids any |
| Boundaries validated | Zod/env schemas in place |
| CI typecheck | tsc --noEmit on every PR |
| Incremental ratchet | Extra flags enabled with no backlog |
Greenfield
Enable strict + noUncheckedIndexedAccess + noImplicitOverride on day one. Add Zod for env and HTTP.
Medium legacy (10–100 files)
allowJs → rename utils first → noImplicitAny → strictNullChecks → full strict. Track errors in a spreadsheet.
Monorepo
| 1 | // packages/tsconfig/base.json |
| 2 | { |
| 3 | "compilerOptions": { |
| 4 | "strict": true, |
| 5 | "noUncheckedIndexedAccess": true, |
| 6 | "composite": true, |
| 7 | "declaration": true, |
| 8 | "declarationMap": true |
| 9 | } |
| 10 | } |
Packages extend base. Ratchet flags in base only when all packages pass.
Incremental file suppressions
| 1 | // Last resort — file-level while migrating a monster module |
| 2 | // @ts-nocheck |
| 3 | // Prefer per-line @ts-expect-error with ticket refs |
| 1 | type User = { id: string; name?: string | null }; |
| 2 | |
| 3 | function displayName(u: User): string { |
| 4 | return u.name?.trim() || "Anonymous"; |
| 5 | } |
| 6 | |
| 7 | function mustId(u: User | undefined): string { |
| 8 | if (!u) throw new Error("missing user"); |
| 9 | return u.id; |
| 10 | } |
| 11 | |
| 12 | // Narrowing arrays |
| 13 | function compact<T>(items: (T | null | undefined)[]): T[] { |
| 14 | return items.filter((x): x is T => x != null); |
| 15 | } |
best practice
| 1 | # GitHub Actions sketch |
| 2 | - name: Typecheck |
| 3 | run: npx tsc --noEmit -p tsconfig.json |
| 1 | // .vscode/settings.json |
| 2 | { |
| 3 | "typescript.tsdk": "node_modules/typescript/lib", |
| 4 | "typescript.enablePromptUseWorkspaceTsdk": true |
| 5 | } |
Pin TypeScript version in the repo so CI and editors agree.
A 50-file Express app enables strictNullChecks. Strategy:
1. Turn on the flag; collect errors by directory.
2. Fix leaf utils (string helpers) first — they unblock many callers.
3. Add | null to DTOs that truly allow null from DB.
4. Replace bang operators with guards as you touch files.
| 1 | // before |
| 2 | function getUser(id: string): User { |
| 3 | return db.find(id); // User | undefined actually |
| 4 | } |
| 5 | // after |
| 6 | function getUser(id: string): User | undefined { |
| 7 | return db.find(id); |
| 8 | } |
| 9 | function requireUser(id: string): User { |
| 10 | const u = getUser(id); |
| 11 | if (!u) throw new Error("not found"); |
| 12 | return u; |
| 13 | } |
End-to-end snippets for strict. Type these into a strict project and mutate them until the checker teaches you the edges.
Example A
| 1 | type Id = string & { readonly __brand: unique symbol }; |
| 2 | function asId(s: string): Id { return s as Id; } |
| 3 | |
| 4 | type Entity = { id: Id; createdAt: Date }; |
| 5 | function touch(e: Entity): Entity { |
| 6 | return { ...e, createdAt: new Date() }; |
| 7 | } |
Example B — conditional distribute
| 1 | type IsString<T> = T extends string ? true : false; |
| 2 | type A = IsString<"hi" | 1>; // true | false (distributed) |
| 3 | |
| 4 | type IsStringBox<T> = [T] extends [string] ? true : false; |
| 5 | type B = IsStringBox<"hi" | 1>; // false |
Example C — key remapping filter
| 1 | type OnlyStrings<T> = { |
| 2 | [K in keyof T as T[K] extends string ? K : never]: T[K]; |
| 3 | }; |
| 4 | type User = { id: string; age: number; name: string }; |
| 5 | type Names = OnlyStrings<User>; // { id: string; name: string } |
Example D — template route params
| 1 | type ExtractParams<S extends string> = |
| 2 | S extends `${string}:${infer P}/${infer Rest}` |
| 3 | ? P | ExtractParams<Rest> |
| 4 | : S extends `${string}:${infer P}` |
| 5 | ? P |
| 6 | : never; |
| 7 | type Params = ExtractParams<"/users/:id/posts/:postId">; // "id" | "postId" |
info
Answer without looking. Then verify in the playground / tsc.
| # | Question |
|---|---|
| 1 | What is the difference between annotation, assertion, and satisfies? |
| 2 | When does a conditional type distribute over a union? |
| 3 | How do you prove exhaustiveness for a discriminated union? |
| 4 | Why is unknown safer than any at JSON boundaries? |
| 5 | What does noUncheckedIndexedAccess change about obj[key]? |
| 6 | How do you preserve literal types through a generic helper? |
| 7 | When should you prefer interface over type (and vice versa)? |
| 8 | How does z.infer keep runtime and types aligned? |
best practice
| Flag | Recommendation |
|---|---|
| noUncheckedIndexedAccess | On for apps |
| exactOptionalPropertyTypes | On when ready for churn |
| noImplicitOverride | On for class-heavy code |
| verbatimModuleSyntax | On for modern ESM |
| isolatedModules | On with bundlers |
Extended reference material for deliberate practice. Work through each snippet under strict: true and note where inference surprises you.
Snippet 1 — deep-readonly
Practice snippet 1. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type DeepReadonly<T> = { |
| 2 | readonly [K in keyof T]: T[K] extends (infer U)[] |
| 3 | ? readonly DeepReadonly<U>[] |
| 4 | : T[K] extends object |
| 5 | ? DeepReadonly<T[K]> |
| 6 | : T[K]; |
| 7 | }; |
Snippet 2 — uti
Practice snippet 2. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type UnionToIntersection<U> = |
| 2 | (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never; |
| 3 | type I = UnionToIntersection<{ a: 1 } | { b: 2 }>; // { a:1 } & { b:2 } |
Snippet 3 — narrow-key
Practice snippet 3. Predict the resulting type, then confirm with hover types in your editor.
| 1 | function narrowKey<T extends object, K extends keyof T>( |
| 2 | obj: T, |
| 3 | key: K, |
| 4 | guard: (v: unknown) => boolean, |
| 5 | ): obj is T & Record<K, T[K]> { |
| 6 | return guard(obj[key]); |
| 7 | } |
Snippet 4 — as-tuple
Practice snippet 4. Predict the resulting type, then confirm with hover types in your editor.
| 1 | const asTuple = <const T extends readonly unknown[]>(xs: T) => xs; |
| 2 | const t = asTuple([1, "a", true]); |
Snippet 5 — json
Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type Json = |
| 2 | | string |
| 3 | | number |
| 4 | | boolean |
| 5 | | null |
| 6 | | Json[] |
| 7 | | { [key: string]: Json }; |
Snippet 6 — entries
Practice snippet 6. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type Entries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T]; |
| 2 | type E = Entries<{ a: 1; b: string }>; // ["a", 1] | ["b", string] |
Snippet 7 — atleast
Practice snippet 7. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type RequireAtLeastOne<T> = { |
| 2 | [K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>; |
| 3 | }[keyof T]; |
| 4 | type Patches = RequireAtLeastOne<{ a?: number; b?: string }>; |
Snippet 8 — mutable-keys
Practice snippet 8. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type MutableKeys<T> = { |
| 2 | [K in keyof T]-?: Equal<{ -readonly [P in K]: T[K] }, { [P in K]: T[K] }> extends true ? K : never; |
| 3 | }[keyof T]; |
| 4 | type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false; |
Snippet 9 — snake
Practice snippet 9. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type Snake<S extends string> = S extends `${infer H}${infer T}` |
| 2 | ? T extends Uncapitalize<T> |
| 3 | ? `${Lowercase<H>}${Snake<T>}` |
| 4 | : `${Lowercase<H>}_${Snake<Uncapitalize<T>>}` |
| 5 | : S; |
Snippet 10 — publicof
Practice snippet 10. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type PublicOf<T> = { [K in keyof T]: T[K] }; |
| 2 | class Svc { #secret = 1; public open = true; } |
| 3 | type P = PublicOf<Svc>; |
note
Extended reference material for deliberate practice. Work through each snippet under strict: true and note where inference surprises you.
Snippet 1 — deep-readonly
Practice snippet 1. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type DeepReadonly<T> = { |
| 2 | readonly [K in keyof T]: T[K] extends (infer U)[] |
| 3 | ? readonly DeepReadonly<U>[] |
| 4 | : T[K] extends object |
| 5 | ? DeepReadonly<T[K]> |
| 6 | : T[K]; |
| 7 | }; |
Snippet 2 — uti
Practice snippet 2. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type UnionToIntersection<U> = |
| 2 | (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never; |
| 3 | type I = UnionToIntersection<{ a: 1 } | { b: 2 }>; // { a:1 } & { b:2 } |
Snippet 3 — narrow-key
Practice snippet 3. Predict the resulting type, then confirm with hover types in your editor.
| 1 | function narrowKey<T extends object, K extends keyof T>( |
| 2 | obj: T, |
| 3 | key: K, |
| 4 | guard: (v: unknown) => boolean, |
| 5 | ): obj is T & Record<K, T[K]> { |
| 6 | return guard(obj[key]); |
| 7 | } |
Snippet 4 — as-tuple
Practice snippet 4. Predict the resulting type, then confirm with hover types in your editor.
| 1 | const asTuple = <const T extends readonly unknown[]>(xs: T) => xs; |
| 2 | const t = asTuple([1, "a", true]); |
Snippet 5 — json
Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type Json = |
| 2 | | string |
| 3 | | number |
| 4 | | boolean |
| 5 | | null |
| 6 | | Json[] |
| 7 | | { [key: string]: Json }; |
Snippet 6 — entries
Practice snippet 6. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type Entries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T]; |
| 2 | type E = Entries<{ a: 1; b: string }>; // ["a", 1] | ["b", string] |
Snippet 7 — atleast
Practice snippet 7. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type RequireAtLeastOne<T> = { |
| 2 | [K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>; |
| 3 | }[keyof T]; |
| 4 | type Patches = RequireAtLeastOne<{ a?: number; b?: string }>; |
Snippet 8 — mutable-keys
Practice snippet 8. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type MutableKeys<T> = { |
| 2 | [K in keyof T]-?: Equal<{ -readonly [P in K]: T[K] }, { [P in K]: T[K] }> extends true ? K : never; |
| 3 | }[keyof T]; |
| 4 | type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false; |
Snippet 9 — snake
Practice snippet 9. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type Snake<S extends string> = S extends `${infer H}${infer T}` |
| 2 | ? T extends Uncapitalize<T> |
| 3 | ? `${Lowercase<H>}${Snake<T>}` |
| 4 | : `${Lowercase<H>}_${Snake<Uncapitalize<T>>}` |
| 5 | : S; |
Snippet 10 — publicof
Practice snippet 10. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type PublicOf<T> = { [K in keyof T]: T[K] }; |
| 2 | class Svc { #secret = 1; public open = true; } |
| 3 | type P = PublicOf<Svc>; |
note
Extended reference material for deliberate practice. Work through each snippet under strict: true and note where inference surprises you.
Snippet 1 — deep-readonly
Practice snippet 1. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type DeepReadonly<T> = { |
| 2 | readonly [K in keyof T]: T[K] extends (infer U)[] |
| 3 | ? readonly DeepReadonly<U>[] |
| 4 | : T[K] extends object |
| 5 | ? DeepReadonly<T[K]> |
| 6 | : T[K]; |
| 7 | }; |
Snippet 2 — uti
Practice snippet 2. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type UnionToIntersection<U> = |
| 2 | (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never; |
| 3 | type I = UnionToIntersection<{ a: 1 } | { b: 2 }>; // { a:1 } & { b:2 } |
Snippet 3 — narrow-key
Practice snippet 3. Predict the resulting type, then confirm with hover types in your editor.
| 1 | function narrowKey<T extends object, K extends keyof T>( |
| 2 | obj: T, |
| 3 | key: K, |
| 4 | guard: (v: unknown) => boolean, |
| 5 | ): obj is T & Record<K, T[K]> { |
| 6 | return guard(obj[key]); |
| 7 | } |
Snippet 4 — as-tuple
Practice snippet 4. Predict the resulting type, then confirm with hover types in your editor.
| 1 | const asTuple = <const T extends readonly unknown[]>(xs: T) => xs; |
| 2 | const t = asTuple([1, "a", true]); |
Snippet 5 — json
Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type Json = |
| 2 | | string |
| 3 | | number |
| 4 | | boolean |
| 5 | | null |
| 6 | | Json[] |
| 7 | | { [key: string]: Json }; |
Snippet 6 — entries
Practice snippet 6. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type Entries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T]; |
| 2 | type E = Entries<{ a: 1; b: string }>; // ["a", 1] | ["b", string] |
Snippet 7 — atleast
Practice snippet 7. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type RequireAtLeastOne<T> = { |
| 2 | [K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>; |
| 3 | }[keyof T]; |
| 4 | type Patches = RequireAtLeastOne<{ a?: number; b?: string }>; |
Snippet 8 — mutable-keys
Practice snippet 8. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type MutableKeys<T> = { |
| 2 | [K in keyof T]-?: Equal<{ -readonly [P in K]: T[K] }, { [P in K]: T[K] }> extends true ? K : never; |
| 3 | }[keyof T]; |
| 4 | type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false; |
Snippet 9 — snake
Practice snippet 9. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type Snake<S extends string> = S extends `${infer H}${infer T}` |
| 2 | ? T extends Uncapitalize<T> |
| 3 | ? `${Lowercase<H>}${Snake<T>}` |
| 4 | : `${Lowercase<H>}_${Snake<Uncapitalize<T>>}` |
| 5 | : S; |
Snippet 10 — publicof
Practice snippet 10. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type PublicOf<T> = { [K in keyof T]: T[K] }; |
| 2 | class Svc { #secret = 1; public open = true; } |
| 3 | type P = PublicOf<Svc>; |
note
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.