Technology · TypeScript
TypeScript Strict Mode
Understand strict mode benefits, gotchas, and how to enable it progressively.
TL;DR
- 01Enable "strict": true in tsconfig.json for maximum type safety.
- 02Strict mode includes multiple flags that can be enabled individually.
- 03Migrate existing code gradually by enabling flags one at a time.
Enabling Strict Mode
- Enable strict mode to enable all strict flags at once.
{ "compilerOptions": { "strict": true } } - Strict mode enables these flags simultaneously.
{ "compilerOptions": { "noImplicitAny": true, "noImplicitThis": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true } } - New TypeScript projects should use strict mode from the start.
{ "compilerOptions": { "strict": true, "target": "ES2020", "module": "ESNext" } } - Existing projects can migrate gradually by enabling individual flags.
{ "compilerOptions": { "strict": false, "noImplicitAny": true } } - Use noEmitOnError to prevent emitting code when type errors exist.
{ "compilerOptions": { "strict": true, "noEmitOnError": true } }
Implicit Any Errors
- Require types for variables without explicit type annotations.
// With noImplicitAny: error function add(a, b) { return a + b; } // Fixed: explicit types function add(a: number, b: number): number { return a + b; } - Enable noImplicitAny to catch typing mistakes early.
// Error: parameter 'event' implicitly has an 'any' type document.addEventListener("click", (event) => { }); // Fixed document.addEventListener("click", (event: MouseEvent) => { }); - Annotate callback parameters that TypeScript cannot infer.
// Error: 'item' implicitly has type 'any' const items = JSON.parse(data); items.forEach((item) => console.log(item)); // Fixed: type the parsed value const items: string[] = JSON.parse(data); items.forEach((item: string) => console.log(item)); - Use unknown instead of any when the type is genuinely unknown.
function process(input: unknown) { if (typeof input === "string") { console.log(input.toUpperCase()); // Safe after narrowing } } - Reduces bugs from unintended type mismatches throughout the codebase.
// Without noImplicitAny: silent runtime bug function double(n) { return n * 2; } double("3"); // "33" at runtime — no compile-time error
Null Check Errors
- Treat null and undefined as distinct types.
// With strictNullChecks: error const name: string = null; // Fixed: include null in type const name: string | null = null; - Require null checks before accessing properties.
function getName(user: User | null) { if (!user) return "Guest"; return user.name; // Safe: user is not null here } - Prevents many common runtime errors from unchecked nulls.
// Error: Object is possibly undefined const first = items[0].name; // Fixed const first = items[0]?.name ?? "Unknown"; - Enable strictNullChecks independently before enabling full strict mode.
{ "compilerOptions": { "strictNullChecks": true } } - Most impactful strict flag — catching nullable bugs early saves debugging time.
// Without strictNullChecks: no error, crashes at runtime const user = getUser(); // may return null console.log(user.name); // TypeError: Cannot read properties of null
Property Init Errors
- Require class properties to be initialized in the constructor.
// With strictPropertyInitialization: error class User { name: string; } // Fixed: initialize in constructor class User { name: string; constructor(name: string) { this.name = name; } } - Use definite assignment assertion (!) if initialization is deferred.
class User { name!: string; // tells TypeScript: trust me, this will be set async init() { this.name = await fetchName(); } } - Initialize properties with a default value to satisfy the compiler.
class Config { retries: number = 3; timeout: number = 5000; debug: boolean = false; } - Use readonly with constructor initialization for immutable properties.
class Product { readonly id: string; readonly name: string; constructor(id: string, name: string) { this.id = id; this.name = name; } } - Mark properties that are set outside the constructor with declare if needed.
class Widget { declare element: HTMLElement; // set by framework before use render() { this.element.innerHTML = "Hello"; } }
Progressive Migration
- Disable strict mode initially for large migrations.
{ "compilerOptions": { "strict": false } } - Enable individual flags gradually as code is typed.
{ "compilerOptions": { "strict": false, "noImplicitAny": true } } - Use stricter settings for new code while migrating old code.
{ "compilerOptions": { "strict": false }, "ts-node": { "compilerOptions": { "strict": true } } } - Gradually increase strictness until full strict mode is enabled.
{ "compilerOptions": { "noImplicitAny": true, "strictNullChecks": true, "strictPropertyInitialization": true, "strict": true } } - Use @ts-strict-ignore comments to suppress errors in files not yet migrated.
// @ts-strict-ignore // This file is pending strict migration — tracked in issue #42 export function legacyHelper(data) { return data.value; }
Tip: Enable strict mode from the start on new projects — fixing types upfront is easier than retrofitting them later.
Warning: Enabling strict mode on large existing codebases requires significant effort — plan for gradual migration over multiple sprints.