TypeScript strict mode: the 8 settings we never turn off
TypeScript's strict: true flag turns on 8 separate checks. Most codebases keep strict on at the root and quietly disable one or two of them inside a problem file. Here is each of the 8, what it catches in production, and the three flags outside the bundle that we still turn on for every project.
TypeScript strict mode is a single flag in tsconfig.json that turns on eight separate compiler checks at once. The eight are strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, and alwaysStrict. The Microsoft TSConfig reference lists them in the strict family and the Microsoft team has an open proposal (issue 62333) to make strict the default in a future major release.
Most codebases ship with strict: true at the root and a handful of files or modules that quietly opt out of one or two of them. The opt-outs almost never come back. This article is the list of eight, with the failure each one prevents and the price we have paid for leaving it on. None of them is free. None of them has ever cost us more than the bug it caught.
Why the bundle matters
The strict flag is the only stable contract. If a team disables one of the eight, every reader of the code now has to remember which one was switched off. Every dependency that types itself against strict gets one extra hole. Every junior developer joining the team relearns the same gotcha. The bundle is not a checklist; it is a public statement about how a codebase reasons about types. Once one flag is off, the others slowly follow.
The 8 checks inside strict
1. strictNullChecks
What it catches. null and undefined as types distinct from every other type. With it off, a function that returns User implicitly returns User | null | undefined, and every dereference is unsafe.
The cost. Every value from an API response, every optional prop, every Array.prototype.find() return needs a narrowing check.
Why we keep it on. Every codebase we have inherited running with strictNullChecks: false ships a Cannot read properties of undefined bug to production at least once per quarter. The runtime cost of one such crash exceeds the dev cost of one if check by several orders of magnitude. This is the single most important type-system feature in TypeScript, and the migration advice from the community is consistent: turn this one on last because it produces the most errors, and never turn it off again.
2. noImplicitAny
What it catches. Parameters and return values that have no annotation and cannot be inferred. With it off, those slots silently become any.
The cost. Extra typing on quick helpers, especially during migration from JavaScript.
Why we keep it on. any is a virus. A single any in a deep helper deletes types across every caller, often without warning. Every team I have worked with that disabled noImplicitAny for the migration ended up with most new code untyped a year later. The flag is the only mechanism that forces the team to make a conscious decision when a type is unknown, instead of slipping into any by inertia.
3. strictFunctionTypes
What it catches. Function parameter contravariance violations. A function that takes a Dog cannot be assigned to a variable expecting a function that takes any Animal, because the wider type would call it with values the narrower function cannot handle.
The cost. Occasional friction with library types written before this flag existed. The flag also does not apply to method-syntax parameters, so the noisiest cases stay quiet.
Why we keep it on. Callback bugs are silent at runtime. A handler that expects the wrong subtype gets called with values it cannot handle and either crashes deep in a render or silently does the wrong thing. In our own code we have never had a strictFunctionTypes error that turned out to be a false positive.
4. strictBindCallApply
What it catches. Wrong argument types passed to .bind(), .call(), or .apply().
The cost. Almost zero in modern codebases, because nobody writes .bind() anymore.
Why we keep it on. The flag is free in code that uses arrow functions, class methods, and React hooks. Turning it off would only matter for legacy code that should be modernised anyway. There is no upside to disabling it.
5. strictPropertyInitialization
What it catches. Class fields that are declared with a non-undefined type but never assigned in the constructor. Without this check, the field is silently undefined at the first read.
The cost. Fields must be initialised in the constructor, marked with the definite-assignment operator (!), or typed to include undefined.
Why we keep it on. The alternative is fields that look initialised in the type but are not at runtime, which surface as bugs in test setup and DI containers. The ! operator gives an escape hatch for cases where a framework guarantees initialisation, and the requirement to type it out makes the assumption visible. The check pays for itself in any project that uses class-based controllers, ORM entities, or service objects.
6. noImplicitThis
What it catches. this expressions whose type the compiler cannot infer, typically in callbacks passed to forEach or in standalone functions used as event handlers.
The cost. Zero in modern React and Node code that uses arrow functions and ES classes. Real in legacy class-heavy code, which is exactly where it catches the most bugs.
Why we keep it on. A wrongly-bound this is one of the oldest bug families in JavaScript. If TypeScript catches it for free, there is no argument for opting out.
7. useUnknownInCatchVariables
What it catches. The err in a catch (err) block. With this flag, err is typed as unknown instead of any, so you cannot dereference it without narrowing first.
The cost. One if (err instanceof Error) line per catch block.
Why we keep it on. any in a catch block is one of the most common ways any leaks into the rest of the code. err.message gets used in a UI string, then in a log payload, then in a Sentry breadcrumb, and now any has spread across four files. Narrowing once at the catch boundary keeps the rest of the surface honest.
8. alwaysStrict
What it catches. Ensures every parsed file is in strict mode and emits "use strict" in the generated JavaScript.
The cost. Zero in any code written this decade.
Why we keep it on. It is on by default in any module-shaped code (ESM, CommonJS modules, classes). The flag mostly matters for scripts and inline blocks. Turning it off is a smell that someone is shipping plain scripts, which is its own problem.
Three flags outside strict that we also turn on
The strict bundle covers the floor. Three flags sit outside it and have a real impact on production bug counts. We turn them on for every new project from day one.
noUncheckedIndexedAccess
Forces obj[key] and arr[i] to be typed as T | undefined. The default behaviour assumes the access always succeeds, which is wrong for any record or array whose shape is dynamic. The official tsconfig docs spell this out on the noUncheckedIndexedAccess page, and issue 49169 in the TypeScript repo is a long-running discussion of whether it should be folded into strict by default. From our own work: this flag adds the most type-check pressure of any single setting and removes the largest single class of runtime crash we still see in code with strict: true alone.
exactOptionalPropertyTypes
Separates "the property might not be set" from "the property is set to undefined". Most teams treat these as the same thing. They are not, especially with React props passed through wrappers and with form state where a field cleared by the user produces an empty string while a field never touched produces a missing key. The flag forces the distinction at the type level and prevents the silent merge that breaks form persistence.
noPropertyAccessFromIndexSignature
Forces obj["key"] for dynamic access and obj.key for declared properties. The friction is real, but the flag prevents the silent fallthrough where a typo in obj.foo returns undefined because the type has an index signature and the typo matches no declared property.
How we migrate a legacy codebase to strict
The migration has the same shape every time. Turn on strict at the root in a branch. Watch the error count. Add a // @ts-nocheck at the top of any file that explodes, log the count, then strip those comments one file per pull request. Do not lower strict to keep CI green; the errors suppressed today are the errors a colleague debugs at 2am next month. If the ratchet stalls, run tsc --noEmit -p tsconfig.strict.json against the changed files only, so new code pays the full price even while old code is still being cleaned up.
The exception we have made, once, is for strictPropertyInitialization in a NestJS codebase where the DI container guarantees field initialisation after construction. Even there, the right answer was the ! operator on the specific fields, not disabling the flag for the whole project.
The one-line summary
Every flag in the strict bundle prevents a class of bug that we have seen in production code more than once. Three flags outside the bundle prevent the most common bugs that survive strict: true. None of the eleven has ever cost us more than the bug it caught. If a team wants to disable one, the right question is which bug they prefer to ship.
Sources
Frequently asked questions
- What are the 8 flags that TypeScript strict mode enables?
- The strict family contains eight flags: strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, and alwaysStrict. Setting strict: true in tsconfig.json turns on all eight at once. Each one targets a specific class of bug, so disabling any single flag while leaving strict: true at the root creates a hidden hole the rest of the codebase no longer guards against.
- Should noUncheckedIndexedAccess be enabled by default in TypeScript?
- We think yes for any new project, even though it is not inside the strict bundle. The default behaviour types obj[key] and arr[i] as if the access always succeeds, which is wrong for any record or array with a dynamic shape. Issue 49169 in the TypeScript repository tracks the long-running discussion about folding it into strict. The cost is real: it produces the most new type errors of any single flag in our experience. The benefit is that it removes the most common runtime crash class that still survives in code with strict: true alone, particularly around API responses, Record lookups, and array indexing.
- Can we enable TypeScript strict mode incrementally on an existing project?
- Yes, and the order matters. Turn on strict at the root in a branch, then add // @ts-nocheck at the top of any file that produces too many errors to fix in one pass. Strip those comments one file per pull request, prioritising files closest to the user (API handlers, form code, payment flows). Inside the strict bundle, leave strictNullChecks for last: it produces the largest single error count and the highest cleanup effort. Do not lower strict to make CI green. Suppression at the file level is reversible; suppression at the config level rarely is.
- Does TypeScript strict mode slow down the compiler?
- Not in any way that matters. The strict flags change how types are checked, not how much work the type-checker does on each file. Build time and tsc --noEmit time are essentially identical with strict on or off. The real cost is in developer time spent fixing the errors strict surfaces, which is a one-time cost on a clean migration and an ongoing tax on new code that pays itself back the first time the flag prevents a production bug.
Studio
Start a project.
One partner for the digital product you need to build. Faster delivery, modern tech, lower costs. One team, one invoice.