Strict mode in TypeScript is the difference between "TypeScript is just JavaScript with types" and "TypeScript actually catches bugs before they hit production." When you set "strict": true in tsconfig.json, you enable a bundle of compiler flags that dramatically improve type safety. But migrating an existing codebase to strict mode can feel overwhelming. Hundreds or thousands of new errors appear overnight. This guide walks you through every strict flag, explains what each one catches, gives you patterns to fix the most common errors, and provides a phased migration strategy that will not block your team for weeks.
The strict flag enables these individual options: strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitAny, noImplicitThis, alwaysStrict, and useUnknownInCatchVariables.
strictNullChecks is the most impactful flag. Without it, null and undefined are assignable to every type. That means a function returning string could return null, and TypeScript would not warn you. With strictNullChecks, you must explicitly handle null: "string | null" for nullable values. Fix pattern: use optional chaining (user?.name), nullish coalescing (value ?? defaultValue), and type guards (if (value !== null)). For function returns, explicitly declare nullable types and handle them at every call site.
noImplicitAny fires when TypeScript cannot infer a type and would default to "any." This catches function parameters without type annotations, variables initialized from untyped sources, and destructured values from untyped objects. Fix pattern: add explicit type annotations. For event handlers: (e: React.ChangeEvent<HTMLInputElement>). For API responses: create interfaces that match your response shapes. For third-party libraries without types, install @types/libraryname or create a declaration file.
strictFunctionTypes enforces contravariant parameter checking for function types. In practical terms, it prevents you from assigning a function that accepts a broad type to a variable expecting a function that accepts a narrow type. This catches subtle bugs in callback hierarchies and event handler typings.
strictPropertyInitialization requires that class properties are assigned in the constructor or have a definite assignment assertion. Fix pattern: initialize in the constructor, use a default value in the declaration, or (as a last resort) use the definite assignment assertion operator: property!: string. Avoid the assertion operator when possible. It tells TypeScript to trust you, which defeats the purpose of strict mode.
noImplicitThis errors when "this" has an implicit "any" type. This catches unbound method references and callbacks where "this" context is lost. Fix pattern: use arrow functions to capture "this" lexically, or add an explicit "this" parameter to function declarations.
useUnknownInCatchVariables types catch clause variables as "unknown" instead of "any." Fix pattern: add type narrowing in catch blocks. Check if the caught value is an instance of Error: if (error instanceof Error) { console.error(error.message); }. For non-Error values, use a type guard or assertion.
Phased migration strategy. Phase 1: enable alwaysStrict and strictBindCallApply. These cause the fewest errors and are quick wins. Phase 2: enable noImplicitAny. This is the most labor-intensive phase. Work through errors file by file, starting with utility modules that many other files depend on. Phase 3: enable strictNullChecks. This will surface the most bugs. Prioritize fixing errors in data-fetching layers and shared components. Phase 4: enable strictFunctionTypes and strictPropertyInitialization. These are usually small cleanup tasks. Phase 5: flip on "strict": true and remove individual flags.
Patterns for elegant strict TypeScript. Use discriminated unions instead of optional fields for state management: type State = { status: 'loading' } | { status: 'success'; data: User } | { status: 'error'; error: string }. Use the satisfies operator to validate object shapes while preserving literal types. Use branded types for IDs: type UserId = string and { readonly __brand: 'UserId' }. Use Zod for runtime validation that automatically generates TypeScript types. Use the NonNullable, Required, and Pick utility types to derive strict subtypes from looser base types.
The investment in strict mode pays for itself within weeks. Every null pointer exception you catch at compile time is one fewer production incident. Every implicit any you eliminate is one fewer runtime type mismatch. Ship strict TypeScript from day one on new projects, and migrate existing projects incrementally using the phased approach above.
Continue Reading
This content is available with BliniBot Pro or as an individual purchase.