Strict Mode Guide
TypeScript's strict mode is a collection of compiler flags that catch more potential runtime errors at compile time. Enabling "strict": true enforces null checks, disallows implicit any types, validates function parameter types, and more. For new projects, it is strongly recommended to enable strict mode from day one; for existing projects, you can migrate gradually by enabling individual flags one at a time.
Quick Start
Add a single line to your tsconfig.json to enable all strict checks. "strict": true is a meta flag equivalent to enabling every flag prefixed with strict in the table below.
{
"compilerOptions": {
"strict": true,
// Bonus flags (not included in "strict"):
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true
}
}
All Strict Mode Flags
The table below lists every flag included in "strict": true plus recommended bonus flags.
| Flag | What It Does | Default | Since |
|---|---|---|---|
strict | Meta flag — enables all strict* options below | false | 2.3 |
strictNullChecks | null and undefined are no longer assignable to other types | false | 2.0 |
noImplicitAny | Disallows implicit any type inference | false | 1.0 |
strictFunctionTypes | Enables contravariant function parameter checks | false | 2.6 |
strictBindCallApply | Type-checks bind, call, and apply arguments | false | 3.2 |
strictPropertyInitialization | Class properties must be initialized in the constructor | false | 2.7 |
noImplicitThis | Raises error on implicit any-typed this | false | 2.0 |
alwaysStrict | Emits "use strict" in every output file | false | 2.1 |
useUnknownInCatchVariables | Catch clause variables are typed unknown instead of any | false | 4.4 |
noUncheckedIndexedAccess | Indexed access returns T | undefined | false | 4.1 |
exactOptionalPropertyTypes | Distinguishes optional properties from undefined values | false | 4.4 |
noImplicitOverride | Overriding methods must use the override keyword | false | 4.3 |
Note: Light-blue rows are bonus flags not included in "strict": true.
strictNullChecks — Null Safety
This is arguably the most important strict flag. When enabled, null and undefined are no longer subtypes of every type. You must use type narrowing or optional chaining (?.) to safely handle nullable values. This catches the vast majority of "Cannot read property of null" runtime errors at compile time.
function greet(name: string | null) {
// Error: Object is possibly 'null'.
console.log(name.toUpperCase());
}
const el = document.getElementById("app");
// Error: Object is possibly 'null'.
el.innerHTML = "Hello";
function greet(name: string | null) {
// Optional chaining
console.log(name?.toUpperCase());
// Type narrowing
if (name !== null) {
console.log(name.toUpperCase());
}
}
// Non-null assertion (use only when certain)
const el = document.getElementById("app")!;
el.innerHTML = "Hello";
noImplicitAny — No Implicit Any
When TypeScript cannot infer the type of a variable or parameter, it falls back to any. With this flag enabled, that implicit fallback becomes a compile error. It forces you to add explicit type annotations to every parameter and variable, dramatically improving type safety across your codebase.
// Error: Parameter 'x' implicitly has an 'any' type.
function double(x) {
return x * 2;
}
// Error: Parameter 'item' implicitly has an 'any' type.
const items = [1, 2, 3];
items.forEach(function (item, i) {
// item is any here
});
function double(x: number): number {
return x * 2;
}
// Arrow functions infer from Array<number>
const items = [1, 2, 3];
items.forEach((item: number, i: number) => {
console.log(item);
});
// Use unknown for truly dynamic values
function parse(input: string): unknown {
return JSON.parse(input);
}
strictFunctionTypes — Contravariant Parameters
This flag ensures function parameter types are checked contravariantly rather than bivariantly. In practice, this means a function accepting a base type cannot be assigned to a function type expecting a derived type. This prevents subtle runtime errors caused by type mismatches in callback functions.
type Handler = (e: MouseEvent) => void;
// Error: Type '(e: Event) => void' is not
// assignable to type 'Handler'.
const handler: Handler = (e: Event) => {
// e.clientX would be undefined at runtime!
console.log(e.type);
};
type Handler = (e: MouseEvent) => void;
const handler: Handler = (e: MouseEvent) => {
console.log(e.clientX); // safe
};
// Or use a broader type for the variable:
type AnyHandler = (e: Event) => void;
const general: AnyHandler = (e: Event) => {
console.log(e.type);
};
strictPropertyInitialization — Class Property Init
When enabled, non-optional class properties must have a default value or be initialized in the constructor. This prevents undefined errors from accessing uninitialized properties. This flag requires strictNullChecks to also be enabled.
class User {
// Error: Property 'name' has no initializer
// and is not definitely assigned.
name: string;
email: string;
constructor() {
// forgot to assign name and email!
}
}
class User {
// 1. Initialize with default
name: string = "";
// 2. Assign in constructor
email: string;
constructor(email: string) {
this.email = email;
}
}
class Service {
// 3. Definite assignment assertion
// (use only when init happens externally)
db!: Database;
async init() { this.db = await connect(); }
}
useUnknownInCatchVariables — Unknown in Catch
By default, the e in catch (e) is typed as any, letting you access any property without checks. With this flag, e becomes unknown, forcing type checks before use. Since JavaScript's throw can throw any value (not just Error objects), this restriction is entirely justified.
try {
JSON.parse(userInput);
} catch (e) {
// e is 'unknown', cannot access .message directly
console.log(e.message); // Error!
}
try {
JSON.parse(userInput);
} catch (e: unknown) {
if (e instanceof Error) {
console.log(e.message); // safe
} else {
console.log("Unknown error:", String(e));
}
}
// Helper function for reuse:
function getErrorMessage(e: unknown): string {
if (e instanceof Error) return e.message;
return String(e);
}
noUncheckedIndexedAccess — Safe Indexing
This flag adds undefined to the return type of index access operations. For example, arr[0] becomes T | undefined instead of T. This prevents runtime errors from out-of-bounds array access or missing object keys. Note: this flag is not included in "strict": true and must be enabled separately.
const arr = [1, 2, 3];
const val = arr[5]; // val: number | undefined
// Error: Object is possibly 'undefined'.
console.log(val.toFixed(2));
const map: Record<string, number> = { a: 1 };
const x = map["missing"]; // x: number | undefined
console.log(x.toFixed(2)); // Error!
const arr = [1, 2, 3];
const val = arr[5];
if (val !== undefined) {
console.log(val.toFixed(2)); // safe
}
// Nullish coalescing for defaults
const map: Record<string, number> = { a: 1 };
const x = map["missing"] ?? 0;
console.log(x.toFixed(2)); // safe, x is number
// Destructuring with default
const [first = 0] = arr;
noImplicitOverride — Explicit Override Keyword
This flag requires the override keyword when overriding base class methods. It prevents accidentally creating new methods instead of overrides when the base class renames a method, which is especially valuable when refactoring large inheritance hierarchies.
class Animal {
move() { console.log("moving"); }
}
class Dog extends Animal {
// Error: This member must have an 'override'
// modifier because it overrides a member
// in the base class 'Animal'.
move() { console.log("running"); }
}
class Animal {
move() { console.log("moving"); }
}
class Dog extends Animal {
override move() { console.log("running"); }
}
// Also catches typos:
class Cat extends Animal {
// Error: This member cannot have an 'override'
// modifier because it is not declared in the
// base class 'Animal'.
override moev() { console.log("sneaking"); }
}
Migration Guide
Migrating a large project from non-strict to full strict mode can be daunting. Here is a proven step-by-step strategy:
- Start with noImplicitAny — This is the most fundamental flag and exposes areas with weak type inference. It typically produces the most errors, but fixes are straightforward: add type annotations.
- Then enable strictNullChecks — This is the most valuable flag. Fixes include adding null checks, using optional chaining
?., and non-null assertions!. - Add remaining flags one at a time — Enable strictPropertyInitialization, strictFunctionTypes, strictBindCallApply in sequence. Add one flag at a time, fix all errors, then move to the next.
- Use
// @ts-expect-errorfor temporary suppression — For complex errors you cannot fix immediately, mark them with// @ts-expect-errorand revisit later. This is better than// @ts-ignorebecause it alerts you when the error is resolved. - Finally switch to
"strict": true— Once all individual flags are enabled, replace them with"strict": trueto automatically include new strict flags added in future TypeScript versions.
Per-file Strict Checking
For JavaScript files, you can use // @ts-check to enable type checking on individual files:
// @ts-check
/** @type {string} */
let name = "Alice";
// Error: Type 'number' is not assignable to type 'string'.
name = 42;
Frequently Asked Questions
Should I use strict mode in new projects?
Absolutely yes. Enabling "strict": true from day one has virtually no extra cost since new code naturally conforms to strict rules. The TypeScript team and community unanimously recommend strict mode for new projects, and all major framework starters (React, Angular, Vue, Next.js) enable it by default.
How do I fix "Object is possibly null"?
There are four common approaches:
1. Type narrowing — Check with if (obj !== null) before use
2. Optional chaining — Use obj?.property for safe access
3. Nullish coalescing — Use obj ?? defaultValue for defaults
4. Non-null assertion — Use obj!, but only when you are certain the value is not null
What is the difference between "strict": true and individual flags?
"strict": true is a meta flag equivalent to enabling all strict-prefixed flags plus noImplicitAny, noImplicitThis, alwaysStrict, and useUnknownInCatchVariables. The key difference: when future TypeScript versions add new strict flags, "strict": true automatically includes them, whereas individually listed flags do not. Therefore, the end goal should always be "strict": true.
Can I enable strict mode gradually?
Absolutely. The recommended approach is to start with noImplicitAny, then add flags one at a time, fixing all errors before adding the next. For very large codebases, you can also use tools like typescript-strict-plugin to enable strict mode on a per-directory basis. The process may take weeks or months, but each step improves code quality.
What is the relationship between strictNullChecks and noUncheckedIndexedAccess?
strictNullChecks ensures types explicitly declared as T | null require null checks, but it does not affect the return types of index access. noUncheckedIndexedAccess goes further by automatically adding undefined to the return type of arr[i] and obj[key], since the compiler cannot guarantee a value exists at that index. They are complementary and should both be enabled.