Strict Mode and Best Practices
Introduction
strict in tsconfig.json is not a single switch—it turns on a family of checks that catch most classes of bugs before production. This chapter explains each major flag, optional stricter options, and day-to-day habits that keep TypeScript codebases maintainable as they grow.
Prerequisites
Start with strict: true
{
"compilerOptions": {
"strict": true
}
}Equivalent to enabling (among others):
strictNullChecksnoImplicitAnystrictFunctionTypesstrictBindCallApplystrictPropertyInitializationnoImplicitThisalwaysStrict
New projects should keep strict: true unless you are migrating legacy JavaScript with a phased plan.
strictNullChecks
Forces explicit handling of null and undefined:
function length(name: string): number {
return name.length;
}
const input: string | null = getName();
// length(input); // Error
function safeLength(name: string | null): number {
return name?.length ?? 0;
}Practice: Use ? for optional properties, | null when absence is intentional, and ?? / narrowing at boundaries (API, DOM, JSON.parse).
noImplicitAny
Parameters and untyped locals cannot silently become any:
// function log(value) { // Error under noImplicitAny
// console.log(value);
// }
function log(value: unknown): void {
console.log(value);
}Practice: Prefer unknown over any for external data; add types at function boundaries.
strictFunctionTypes
Function parameters are checked contravariantly for callbacks—stricter than loose JavaScript:
type Handler = (event: Event) => void;
function listen(handler: Handler): void {
document.addEventListener("click", handler);
}
// listen((e: MouseEvent) => { ... }); // May error: parameter too specificPractice: Match library callback signatures; do not narrow parameter types when assigning to wider handlers.
strictPropertyInitialization
Class fields must be assigned in the constructor (or at declaration):
class User {
id: string;
name: string;
constructor(id: string, name: string) {
this.id = id;
this.name = name;
}
}Use definite assignment ! only when you truly initialize elsewhere (see nullability chapter).
Optional Stricter Flags
| Option | Effect |
|---|---|
noUncheckedIndexedAccess | arr[i] is T | undefined |
exactOptionalPropertyTypes | Distinguish missing vs undefined on optional props |
noImplicitReturns | All code paths must return in typed functions |
noFallthroughCasesInSwitch | Prevent accidental switch fall-through |
noUnusedLocals / noUnusedParameters | Dead code errors (often ESLint too) |
noImplicitOverride | Require override keyword on subclasses |
Example noUncheckedIndexedAccess:
const scores: number[] = [90, 85];
const first = scores[0];
// first: number | undefinedCoding Practices That Pair with strict
1. Model domains with types, not comments
type OrderStatus = "pending" | "paid" | "shipped";
interface Order {
id: string;
status: OrderStatus;
}2. Avoid any and double assertions
// Bad: escape hatch
const data = JSON.parse(raw) as any;
// Better
const data: unknown = JSON.parse(raw);3. Prefer discriminated unions over flags
type State =
| { status: "loading" }
| { status: "ok"; data: string[] }
| { status: "error"; message: string };4. Use readonly where data should not mutate
interface Config {
readonly apiUrl: string;
}5. Export explicit types at module boundaries
export function createUser(input: CreateUserInput): User {
// ...
}Migrating Legacy JavaScript
Phased approach:
- Enable
allowJs+checkJson a folder - Fix errors file by file
- Rename to
.tsand tighten types - Turn on
strictwhenanyis mostly gone
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"strict": false
}
}Temporary strict: false is acceptable during migration; track removal in tickets.
Warning
Do Not Disable strict Permanently for Convenience
Fixing types at the source pays off. Disabling strictNullChecks to silence errors often hides production crashes.
Team Conventions Checklist
-
strict: truein roottsconfig - CI runs
tsc --noEmitornpm run typecheck - No new
anywithout review comment - Shared
tsconfig.base.jsonfor packages - ESLint
@typescript-eslintaligned with TS version -
unknownat JSON/network boundaries
FAQ
Can I enable strict flags one by one?
Yes. strict is a bundle; you can enable strictNullChecks first in legacy codebases.
strict slows development?
Short-term friction, long-term fewer bugs and better editor support.
strict vs ESLint recommended?
Use both—overlap is fine. tsc owns types; ESLint owns style and some logic rules.
Should tests use the same strict config?
Yes—extend the same base tsconfig with test-specific include.
any in third-party types?
skipLibCheck: true skips checking .d.ts in node_modules; your code should still stay strict.
Is strict enough for security?
No. Types do not replace validation, authz, or sanitization at runtime.