|$ curl https://forge-ai.dev/api/markdown?path=docs/typescript/strict
$cat docs/typescript-—-strict-mode-&-migration.md
updated Today·20 min read·published

TypeScript — Strict Mode & Migration

TypeScriptConfigMigrationIntermediate🎯Free Tools
Introduction

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.

What strict: true Enables
FlagCatchesTypical fix
noImplicitAnyMissing annotations becoming anyAnnotate or fix inference
strictNullChecksnull/undefined not in domain typesOptional chaining, unions, guards
strictFunctionTypesUnsafe callback varianceCorrect parameter types
strictBindCallApplyWrong bind/call/apply argsFix arguments
strictPropertyInitializationClass fields unsetInitializer, definite assignment, constructor
noImplicitThisthis: anyArrow functions or annotate this
useUnknownInCatchVariablescatch (e) is unknownNarrow e
alwaysStrictEmit 'use strict'Usually fine
tsconfig-strict.json
JSON
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

noUncheckedIndexedAccess is not inside strict but is highly recommended — indexed access becomes T | undefined.
Enable Flags One by One

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)

nullchecks.ts
TypeScript
1// Before strictNullChecks
2function len(s: string) {
3 return s.length;
4}
5len(null); // runtime boom — allowed without strictNullChecks
6
7// After
8function len2(s: string | null) {
9 return s?.length ?? 0;
10}
Migrating JavaScript → TypeScript

Phase 0 — Tooling

untitled.bash
Bash
1npm install -D typescript @types/node
2npx tsc --init
3# rename incrementally: .js → .ts / .jsx → .tsx

Phase 1 — Coexistence

untitled.json
JSON
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:

untitled.javascript
JavaScript
1/** @param {string} id @returns {Promise<import('./types').User>} */
2export 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

CI gate: tsc --noEmit must stay green. Never merge with increasing error debt.
Common Migration Fixes
Error patternFix
Object is possibly undefinedOptional chain, early return, or ! only if proven
Implicit any parameterAnnotate callback params
Index signature undefinedCheck before use; default
Property missing in classInitialize or use definite assignment assertion carefully
JSON.parse returns anyType as unknown + Zod
fixes.ts
TypeScript
1const map = new Map<string, number>();
2const n = map.get("x"); // number | undefined
3if (n !== undefined) {
4 console.log(n.toFixed(2));
5}
6
7type Dict = Record<string, number>;
8function read(d: Dict, k: string) {
9 const v = d[k]; // number | undefined with noUncheckedIndexedAccess
10 return v ?? 0;
11}
exactOptionalPropertyTypes
exact-optional.ts
TypeScript
1type Opts = { debug?: boolean };
2
3// With exactOptionalPropertyTypes:
4const a: Opts = { debug: true }; // OK
5const b: Opts = { debug: undefined }; // Error — undefined ≠ missing
6const c: Opts = {}; // OK
ESLint Partnership
eslint-snippet.json
JSON
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}
Strict Adoption Checklist
StepDone when
strict: true in base tsconfigAll packages extend it
Zero any in public APIseslint forbids any
Boundaries validatedZod/env schemas in place
CI typechecktsc --noEmit on every PR
Incremental ratchetExtra flags enabled with no backlog
Playbooks by Codebase Size

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

untitled.json
JSON
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

untitled.typescript
TypeScript
1// Last resort — file-level while migrating a monster module
2// @ts-nocheck
3// Prefer per-line @ts-expect-error with ticket refs
Nullish Handling Patterns
nullish.ts
TypeScript
1type User = { id: string; name?: string | null };
2
3function displayName(u: User): string {
4 return u.name?.trim() || "Anonymous";
5}
6
7function mustId(u: User | undefined): string {
8 if (!u) throw new Error("missing user");
9 return u.id;
10}
11
12// Narrowing arrays
13function compact<T>(items: (T | null | undefined)[]): T[] {
14 return items.filter((x): x is T => x != null);
15}

best practice

Prefer explicit unions over non-null assertions (!). Assertions hide future regressions.
CI & Editor Setup
untitled.yaml
YAML
1# GitHub Actions sketch
2- name: Typecheck
3 run: npx tsc --noEmit -p tsconfig.json
untitled.json
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.

Case Study — Enabling strictNullChecks

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.

untitled.typescript
TypeScript
1// before
2function getUser(id: string): User {
3 return db.find(id); // User | undefined actually
4}
5// after
6function getUser(id: string): User | undefined {
7 return db.find(id);
8}
9function requireUser(id: string): User {
10 const u = getUser(id);
11 if (!u) throw new Error("not found");
12 return u;
13}
Worked Examples — strict

End-to-end snippets for strict. Type these into a strict project and mutate them until the checker teaches you the edges.

Example A

untitled.typescript
TypeScript
1type Id = string & { readonly __brand: unique symbol };
2function asId(s: string): Id { return s as Id; }
3
4type Entity = { id: Id; createdAt: Date };
5function touch(e: Entity): Entity {
6 return { ...e, createdAt: new Date() };
7}

Example B — conditional distribute

untitled.typescript
TypeScript
1type IsString<T> = T extends string ? true : false;
2type A = IsString<"hi" | 1>; // true | false (distributed)
3
4type IsStringBox<T> = [T] extends [string] ? true : false;
5type B = IsStringBox<"hi" | 1>; // false

Example C — key remapping filter

untitled.typescript
TypeScript
1type OnlyStrings<T> = {
2 [K in keyof T as T[K] extends string ? K : never]: T[K];
3};
4type User = { id: string; age: number; name: string };
5type Names = OnlyStrings<User>; // { id: string; name: string }

Example D — template route params

untitled.typescript
TypeScript
1type 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;
7type Params = ExtractParams<"/users/:id/posts/:postId">; // "id" | "postId"

info

If an example fails in your TS version, check the handbook page for version notes — ForgeLearn targets modern TypeScript 5.x.
Self-Check Questions

Answer without looking. Then verify in the playground / tsc.

#Question
1What is the difference between annotation, assertion, and satisfies?
2When does a conditional type distribute over a union?
3How do you prove exhaustiveness for a discriminated union?
4Why is unknown safer than any at JSON boundaries?
5What does noUncheckedIndexedAccess change about obj[key]?
6How do you preserve literal types through a generic helper?
7When should you prefer interface over type (and vice versa)?
8How does z.infer keep runtime and types aligned?

best practice

Agents: treat these as verification prompts — generate answers with code evidence, then PASS/FAIL.
Beyond the strict Bundle
FlagRecommendation
noUncheckedIndexedAccessOn for apps
exactOptionalPropertyTypesOn when ready for churn
noImplicitOverrideOn for class-heavy code
verbatimModuleSyntaxOn for modern ESM
isolatedModulesOn with bundlers
Extended Drills (strict) — Set 1

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.

deep-readonly-1.ts
TypeScript
1type 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.

uti-2.ts
TypeScript
1type UnionToIntersection<U> =
2 (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
3type 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.

narrow-key-3.ts
TypeScript
1function 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.

as-tuple-4.ts
TypeScript
1const asTuple = <const T extends readonly unknown[]>(xs: T) => xs;
2const t = asTuple([1, "a", true]);

Snippet 5 — json

Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.

json-5.ts
TypeScript
1type 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.

entries-6.ts
TypeScript
1type Entries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T];
2type 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.

atleast-7.ts
TypeScript
1type RequireAtLeastOne<T> = {
2 [K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>;
3}[keyof T];
4type 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.

mutable-keys-8.ts
TypeScript
1type 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];
4type 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.

snake-9.ts
TypeScript
1type 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.

publicof-10.ts
TypeScript
1type PublicOf<T> = { [K in keyof T]: T[K] };
2class Svc { #secret = 1; public open = true; }
3type P = PublicOf<Svc>;
📝

note

These snippets are intentionally dense. Prefer understanding one fully over skimming all.
Extended Drills (strict) — Set 2

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.

deep-readonly-1.ts
TypeScript
1type 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.

uti-2.ts
TypeScript
1type UnionToIntersection<U> =
2 (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
3type 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.

narrow-key-3.ts
TypeScript
1function 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.

as-tuple-4.ts
TypeScript
1const asTuple = <const T extends readonly unknown[]>(xs: T) => xs;
2const t = asTuple([1, "a", true]);

Snippet 5 — json

Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.

json-5.ts
TypeScript
1type 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.

entries-6.ts
TypeScript
1type Entries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T];
2type 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.

atleast-7.ts
TypeScript
1type RequireAtLeastOne<T> = {
2 [K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>;
3}[keyof T];
4type 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.

mutable-keys-8.ts
TypeScript
1type 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];
4type 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.

snake-9.ts
TypeScript
1type 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.

publicof-10.ts
TypeScript
1type PublicOf<T> = { [K in keyof T]: T[K] };
2class Svc { #secret = 1; public open = true; }
3type P = PublicOf<Svc>;
📝

note

These snippets are intentionally dense. Prefer understanding one fully over skimming all.
Extended Drills (strict) — Set 3

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.

deep-readonly-1.ts
TypeScript
1type 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.

uti-2.ts
TypeScript
1type UnionToIntersection<U> =
2 (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
3type 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.

narrow-key-3.ts
TypeScript
1function 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.

as-tuple-4.ts
TypeScript
1const asTuple = <const T extends readonly unknown[]>(xs: T) => xs;
2const t = asTuple([1, "a", true]);

Snippet 5 — json

Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.

json-5.ts
TypeScript
1type 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.

entries-6.ts
TypeScript
1type Entries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T];
2type 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.

atleast-7.ts
TypeScript
1type RequireAtLeastOne<T> = {
2 [K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>;
3}[keyof T];
4type 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.

mutable-keys-8.ts
TypeScript
1type 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];
4type 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.

snake-9.ts
TypeScript
1type 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.

publicof-10.ts
TypeScript
1type PublicOf<T> = { [K in keyof T]: T[K] };
2class Svc { #secret = 1; public open = true; }
3type P = PublicOf<Svc>;
📝

note

These snippets are intentionally dense. Prefer understanding one fully over skimming all.
$Blueprint — Engineering Documentation·Section ID: TS-STRICT·Revision: 1.0

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.