Enable TypeScript strict mode in tsconfig.json
rule · typescript-strict-mode
TypeScript's strict (opens in a new tab) mode is a single flag that enables a group of compile-time checks covering the most common sources of runtime errors. Enabling it from the start of a project costs almost nothing; adding it later can require fixing hundreds of type errors.
Code Example
{
"compilerOptions": {
// Strict mode umbrella — enables all flags below
"strict": true,
// What "strict": true enables individually:
// "strictNullChecks": true — null and undefined are not valid everywhere
// "noImplicitAny": true — variables must have an explicit or inferred type
// "strictFunctionTypes": true — function parameter types are checked contravariantly
// "strictBindCallApply": true — .bind()/.call()/.apply() are type-checked
// "strictPropertyInitialization": true — class properties must be initialised in constructors
// "noImplicitThis": true — 'this' must have an explicit type
// "useUnknownInCatchVariables": true — catch clause variables are 'unknown', not 'any'
// "alwaysStrict": true — emits 'use strict' in every output file
// Strongly recommended additions (not included in "strict")
"noUncheckedIndexedAccess": true, // array[0] returns T | undefined
"noImplicitOverride": true, // override keyword required when overriding
"exactOptionalPropertyTypes": true, // { x?: string } does not accept undefined
"noFallthroughCasesInSwitch": true, // switch fallthrough is a compile error
// Project setup
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"sourceMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Why It Matters
Without strict mode, TypeScript allows implicit any types, ignores possible null or undefined values, and silently accepts unsound function assignments — meaning the type system gives a false sense of safety. The TSConfig strict reference (opens in a new tab) and the TypeScript handbook's core type model (opens in a new tab) both frame strict mode as the baseline if you want type errors to fail at compile time instead of in production.
Bug Caught by strictNullChecks
The most impactful strict flag is strictNullChecks. Without it, TypeScript silently allows null and undefined to be assigned to any type.
// ❌ Without strictNullChecks — TypeScript accepts this, crashes at runtime
function getUsername(userId: string): string {
const user = findUser(userId) // returns User | null
return user.name // TypeError: Cannot read properties of null
}
// ✅ With strictNullChecks — TypeScript forces you to handle the null case
function getUsername(userId: string): string {
const user = findUser(userId) // TypeScript infers: User | null
if (!user) {
return 'Unknown' // must handle null before accessing .name
}
return user.name // safe: user is User here
}
// ✅ Alternative with optional chaining and nullish coalescing
function getUsername(userId: string): string {
return findUser(userId)?.name ?? 'Unknown'
}Bug Caught by noImplicitAny
// ❌ Without noImplicitAny — parameter 'data' silently becomes any
function processData(data) {
return data.items.map((item) => item.value) // no type checking at all
}
// ✅ With noImplicitAny — must declare the type
interface DataPayload {
items: Array<{ value: number }>
}
function processData(data: DataPayload): number[] {
return data.items.map((item) => item.value) // fully type-checked
}Bug Caught by useUnknownInCatchVariables
// ❌ Before TypeScript 4.4 / without strict — error is implicitly 'any'
try {
await fetchData()
} catch (error) {
console.log(error.message) // no type checking — could crash if error is not an Error
}
// ✅ With useUnknownInCatchVariables — error is 'unknown', forces a check
try {
await fetchData()
} catch (error) {
if (error instanceof Error) {
console.log(error.message) // safe
} else {
console.log('Unknown error', error)
}
}Incremental Adoption on Existing Codebases
If adding "strict": true to an existing project produces hundreds of errors, enable flags one at a time. Start with the highest-value flags first.
// Phase 1 — highest value, usually fixable quickly
{
"compilerOptions": {
"strictNullChecks": true
}
}// Phase 2 — add once phase 1 errors are resolved
{
"compilerOptions": {
"strictNullChecks": true,
"noImplicitAny": true
}
}// Phase 3 — full strict mode
{
"compilerOptions": {
"strict": true
}
}strictPropertyInitialization in Classes
// ❌ Class property not initialised — runtime access returns undefined
class UserService {
private client: ApiClient // strict error: not definitely assigned
async getUser(id: string) {
return this.client.get(`/users/${id}`) // TypeError at runtime
}
}
// ✅ Initialised in constructor
class UserService {
private client: ApiClient
constructor(client: ApiClient) {
this.client = client
}
async getUser(id: string) {
return this.client.get(`/users/${id}`)
}
}
// ✅ Definite assignment assertion (when you know it will be set before use)
class UserService {
private client!: ApiClient // '!' tells TypeScript: trust me, this is set
init(client: ApiClient) {
this.client = client
}
}Exceptions
- Generated types, framework scaffolding, or narrow interoperability layers can justify exceptions, but they should be isolated and documented rather than normalized across the codebase.
- A type-safety smell is often secondary to a stronger runtime validation or API design issue; fix the layer that removes the real user risk first.
- Do not suppress a type rule broadly when a smaller wrapper, adapter, or validated boundary would contain the exception.
Verification
Use the TSConfig strict reference (opens in a new tab) as the source of truth for which checks the compiler should enforce, and keep the typed-linting guidance (opens in a new tab) aligned with that setup so editor and CI behavior match.
- Open
tsconfig.jsonand confirm"strict": trueis present insidecompilerOptions, or verify all individual strict flags are explicitly set. - Run
pnpm exec tsc --noEmitand confirm zero type errors in the project. If errors appear, fix them rather than suppressing them. - Check that
tsconfig.jsonis not overriding strict flags after the"strict": truedeclaration (e.g.,"strictNullChecks": falsewould silently disable the check). - Confirm the CI pipeline runs
tsc --noEmitso that future code changes cannot silently disable type safety.