Engineering
TypeScript strict mode in production — why it's not optional
Teams that skip strict mode rationalise it as pragmatism. It's not. It's deferred debugging. Here's what strict mode catches before production, what it costs to adopt mid-project, and why we enable it on day one of every codebase.
Every TypeScript project we inherit from another team has the same tsconfig.json:
{
"compilerOptions": {
"strict": false,
"noImplicitAny": false,
"strictNullChecks": false
}
}
And every one of those projects has the same class of production bugs: undefined is not a function, cannot read property of null, and the classic NaN silently propagating through a calculation. These are not exotic. They are the direct consequence of skipping strict mode.
We enable strict mode on day one of every codebase. Here is why, what it costs, and the arguments against it that don't hold up.
What strict mode actually catches
TypeScript's strict: true enables seven sub-flags. Three of them do almost all the work:
strictNullChecks is the single most underrated compiler flag in the JavaScript ecosystem. Without it, null and undefined are assignable to every type. A function that returns string can return undefined. A property that is typed User can be null. The compiler will not warn you.
With it enabled, this code fails:
// Without strictNullChecks — compiles, crashes in production
function getUserName(user: User): string {
return user.name; // user could be null
}
// With strictNullChecks — compiler error
function getUserName(user: User): string {
// Error: 'user' is possibly 'null'
return user.name;
}
The fix is two characters:
function getUserName(user: User): string {
return user?.name ?? "Unknown";
}
Every production undefined is not a function error we have debugged in the last six months would have been caught by strictNullChecks at compile time. Most of them were in codebases where it was disabled.
noImplicitAny catches the cases where you didn't provide a type and TypeScript inferred any. This is the flag that forces you to type your function parameters, return types, and generic arguments. It is annoying. It also catches the bugs where you thought you were working with a User but the inference gave you any because of a missing import or a misconfigured API response type.
strictFunctionTypes enforces contravariance on function parameter types. This matters most with callbacks and event handlers. Without it, you can pass a function that accepts any to a handler that expects MouseEvent, and nothing complains — until the function accesses a property that doesn't exist on the actual event.
What it costs to adopt mid-project
We have converted three codebases from strict: false to strict: true. Here is the honest cost.
On a 10,000-line codebase with decent existing types: 2-3 days of work. Most errors are null checks on optional properties and missing return type annotations. The fixes are mechanical. The hardest part is the 20-odd places where the code genuinely relied on null propagating silently and now needs restructuring.
On a 50,000-line codebase with weak existing types: 1-2 weeks. The noImplicitAny errors cascade — fixing one reveals three more. The strictNullChecks errors force you to decide, for every nullable field, whether it should genuinely be nullable or whether the data model is wrong. That decision is the real work.
On a codebase with no types at all: Do not convert directly. Migrate file by file, renaming .js to .ts and fixing errors as you go. The incremental path is the only path.
The cost of adoption mid-project is real. But the cost of not adopting is paid in production incidents, each of which is more expensive than the migration, measured in engineering time and customer trust.
The arguments against strict mode
We have heard them all. Here are the ones that come up and why we disagree.
"It slows down development." It does, by about 5-10% on a new project. That 5-10% is time spent adding type annotations and null checks that the compiler demands. But the alternative is spending that time — and more — debugging runtime errors that the type system would have prevented. The net is faster, not slower, because runtime debugging is the most expensive form of development.
"It's too strict for prototyping." This one has some truth. If you are building a throwaway proof of concept that will be deleted in a week, skip strict mode. But if there is any chance the prototype graduates to production — and there always is — the cost of enabling strict mode later is higher than the cost of enabling it now. We have learned this the hard way. Every prototype we skipped strict mode on became a production codebase. Every one required a migration later.
"Our library types are not strict-compatible." This was true in 2021. It is barely true in 2026. The major ecosystem — React 19, Next.js 16, Zod, tRPC, TanStack Query — ships strict-compatible types. If you hit a library that is not, the pattern is:
// Declare a local module augmentation
declare module "untyped-library" {
export function doThing(input: any): any;
}
Or better: fork the types, fix them, and upstream the PR. The ecosystem is not static. Help it improve.
"The team doesn't know TypeScript well enough." This is the one we have most sympathy for. TypeScript's type system is powerful and sometimes intimidating. But strict mode is actually easier for beginners than non-strict mode, because the errors are consistent. In non-strict mode, the same code sometimes passes and sometimes doesn't depending on invisible inference. In strict mode, the rules are predictable. A beginner learns "if it could be null, check for null" once and applies it everywhere.
What we enforce beyond strict mode
Strict mode is the floor. We also enable these in every project tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true
}
}
noUncheckedIndexedAccess is the second-most underrated flag. Without it, array[3] is typed as T. With it, array[3] is typed as T | undefined. Every access to an array or object by index now requires a check. This catches the bug where you assumed a record had a key that it didn't.
exactOptionalPropertyTypes prevents undefined from being explicitly assigned to optional properties. An optional property x?: number accepts x: number and x absent. It does not, with this flag, accept x: undefined. This is a subtle distinction but it matters when you're merging partial updates from an API.
The honest limits
TypeScript strict mode does not prevent logic errors. It does not prevent you from writing if (a > b) when you meant if (a < b). It does not prevent business rule violations. It does not replace tests.
What it does is eliminate an entire category of errors — null reference, undefined property access, type mismatch — that constitute something like 30-40% of production JavaScript bugs by volume. Eliminating 30% of bugs at compile time, for the cost of a one-line config change, is the highest-leverage investment in code quality available to a TypeScript team.
Strict mode is not pedantry. It is the cheapest bug-prevention tool in the TypeScript ecosystem. Enable it on day one. If your project is already running without it, budget the migration. It pays back faster than you think.
Tags
- typescript
- strict-mode
- engineering
- nextjs
- type-safety
More on engineering
- Docker, PM2, nginx — the small-team production stack that worksNot every team needs Kubernetes. For 1-5 person teams shipping to a VPS, the Docker + PM2 + nginx stack is boring, reliable, and cheap. Here's the exact config we run across twelve deployments, including the footguns and the fixes.
- Shipping on honest timelines — the studio's internal disciplinePadding estimates is not honesty. Underestimating is not confidence. The discipline that actually works is writing the plan, measuring against it, and publishing the gap. Here's the exact process we use for every client engagement.
- LangGraph: when the complexity actually pays offLangGraph is the most powerful and most painful agent framework. A walk through when state machines and checkpoints earn their cost, and when you should just use the Claude Agent SDK and move on.