Overview
strict: true is the floor for any TypeScript project, not the ceiling. Every flag inside strict catches a class of real bug that loose code ships to production. This page is the rule set for what to turn on, why, and how to migrate a loose codebase without freezing feature work. Read typescript first for the broader playbook.
Turn on strict: true on day one
Set strict: true in tsconfig.json before you write the second file. Adding it later means fighting hundreds of errors that a fresh project never had to write.
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true
}
}strict is an umbrella that enables noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, and alwaysStrict. Disabling any individual flag to “make the errors go away” is a vote for an untyped codebase.
Treat noUncheckedIndexedAccess as part of strict
Strict mode misses one truth: array indexing returns T | undefined, not T. noUncheckedIndexedAccess fixes that.
const items = ["a", "b", "c"]
// Without the flag: items[10] is typed string. It is undefined at runtime.
// With the flag: items[10] is string | undefined, and the compiler enforces a check.
const value = items[10]
if (value !== undefined) {
console.log(value.toUpperCase())
}This flag is the single biggest source of “we caught a production bug at the type level” wins. Turn it on. See typescript-narrowing for the narrowing patterns that follow.
Use exactOptionalPropertyTypes for honest optionals
By default, an optional property paidAt?: Date accepts both “missing” and “explicitly undefined.” That is a lie; most code treats them differently. exactOptionalPropertyTypes: true forces you to model the distinction.
type Invoice = { id: string; paidAt?: Date }
// With the flag on:
const a: Invoice = { id: "1" } // OK: paidAt missing
const b: Invoice = { id: "1", paidAt: undefined } // Error: undefined is not Date
// If undefined is a real value, model it:
type Invoice2 = { id: string; paidAt?: Date | undefined }The flag forces you to decide whether undefined is a sentinel or an oversight. Pick one.
Use noImplicitOverride for inheritance hygiene
When a subclass redefines a method, write override on the redefinition. The compiler checks that the parent actually has a method by that name; rename refactors stop silently breaking subclasses.
class Base {
greet() { return "hi" }
}
class Child extends Base {
override greet() { return "hello" } // typo in the parent name now fails to compile
}This costs nothing and prevents the silent-shadow bug that bites every long-lived class hierarchy. See general-principles for the wider rules on inheritance and composition.
Treat catch variables as unknown
useUnknownInCatchVariables (part of strict) types caught errors as unknown. Narrow before use.
try {
await loadInvoice(id)
} catch (err) {
if (err instanceof Error) {
logger.error({ message: err.message, stack: err.stack })
return
}
logger.error({ message: String(err) })
}The old any behavior let err.statusCode compile when err was a string. The new behavior makes you check. See typescript-narrowing for the instanceof and in patterns.
Migrate a loose codebase incrementally
Flipping strict: true on a million-line codebase produces ten thousand errors and a frozen team. The migration path:
- Set
strict: trueandnoUncheckedIndexedAccess: trueon a newtsconfig.strict.jsonthatextendsthe main config and lists only the files that already pass. - As you touch each file, fix the errors and move it from the loose list to the strict list.
- Use
// @ts-expect-error(never@ts-ignore) for legitimate temporary holes; the compiler will flag the comment once the error goes away. - When the strict list covers the whole codebase, delete the loose config.
Track migration progress with a count of files in the strict list. Avoid the temptation to ship strict: false with hand-written as any casts; that produces an untyped codebase that pretends to be typed.
Audit any and as casts in code review
Strict mode is undone by one as any per file. Treat any, as unknown as T, and // @ts-ignore as code-review smells. Each one needs a comment explaining why it is the right escape hatch.
// Bad
const config = JSON.parse(raw) as Config
// Better: validate at the boundary.
const config = ConfigSchema.parse(JSON.parse(raw))See typescript-runtime-validation for the schema-at-the-boundary rule that removes most casts. For the umbrella rules, see typescript.