TypeScript strict mode: the 6 tsconfig options that actually matter in production and when to enable them
There's a scene that plays out over and over. Someone sets up a new project, tells everyone "we're using strict TypeScript," and drops strict: true into tsconfig.json. Everyone nods. CI compiles. And three months later there's a production bug that TypeScript could have caught if someone had bothered to enable noUncheckedIndexedAccess.
My take is blunt: strict: true is a comfortable shortcut that enables six reasonable flags but leaves out two options that, in my experience, prevent more silent bugs than half the base group combined. The problem isn't strict: true itself — it's that most people enable it and feel like they're done.
This post isn't "enable strict and move on." It's a flag-by-flag breakdown: what each one does, what kind of error it prevents, and the sensible order for migrating a codebase that doesn't have them all enabled yet.
What strict: true includes — and what it doesn't
According to the are clear on this. Why isn't it in strict? Because it generates a lot of errors in existing codebases where index access is everywhere and nobody validates it. But that doesn't make it optional if you want real coverage.
In scenarios involving Prisma query results, external API responses cast to arrays, or configuration read from JSON — this flag catches exactly the class of bug that shows up late, in production, the first time the array arrives empty.
6. exactOptionalPropertyTypes — the most undervalued of all
This is the second one most people ignore, and the one that breaks things most subtly. Without this flag, TypeScript treats undefined as a valid value for an optional property. With it, there's a real difference between "the property might not be there" and "the property is there and equals undefined."
interface Config {
timeout?: number; // optional property
}
// Without exactOptionalPropertyTypes:
// These two assignments are equivalent to TypeScript:
const a: Config = {}; // timeout doesn't exist
const b: Config = { timeout: undefined }; // timeout exists but is undefined
// With exactOptionalPropertyTypes:
const c: Config = { timeout: undefined }; // ❌ Error
// Type 'undefined' is not assignable to type 'number'
// because 'timeout?' means 'might not be present', not 'can be undefined'
Why does this matter? Because there's an operational difference between a missing key and a key with value undefined. In JSON serialization, in object spreads, in Prisma updates — the behavior differs. exactOptionalPropertyTypes makes TypeScript understand that distinction.
The order to migrate an existing codebase
If you're adding this to a project that already has code, the sensible order is:
Step 1: strictNullChecks → most errors, highest impact, but they're the most urgent ones
Step 2: noImplicitAny → second batch of errors, easier to resolve
Step 3: strict: true → enables the rest of the base group all at once
Step 4: noUncheckedIndexedAccess → new errors, but they're exactly the ones you wanted to see
Step 5: exactOptionalPropertyTypes → last, requires really understanding your data model
A useful strategy for large projects is to enable flags with temporary // @ts-expect-error comments and resolve them file by file. Another is to use skipLibCheck: true during migration so you're not blocked by dependency types that haven't been updated yet.
// tsconfig.json — progressive migration config
{
"compilerOptions": {
// Step 1: start here
"strictNullChecks": true,
// Step 2: once the project compiles with the above
"noImplicitAny": true,
// Step 3: enable the full base group
"strict": true,
// Steps 4 and 5: after stabilizing the base group
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Temporary during migration:
"skipLibCheck": true
}
}
The mistakes people make most often when migrating
Enabling everything at once and then giving up. CI explodes with 400 errors and someone decides "TypeScript strict is too restrictive." The problem isn't the flag — it's the order.
Using as to silence errors instead of fixing them. Every as unknown as WhateverTypeIWant is a type debt. It pushes the error to runtime and makes the migration purely cosmetic.
// This isn't a migration, it's a disguise:
const result = fetchUser() as User; // ❌ Ignores that fetchUser might return null
// This is:
const raw = await fetchUser();
if (!raw) throw new Error("User not found");
const result: User = raw; // ✅
Ignoring the two flags outside strict. This is the most common mistake and the one that motivated this post. A lot of teams declare they're using strict TypeScript without knowing that noUncheckedIndexedAccess isn't included in that preset.
Enabling exactOptionalPropertyTypes without reviewing Prisma updates. In Prisma, updates use optional properties extensively. With this flag, patterns that used to compile stop doing so. That's not a blocker — it's a signal that your data model was imprecise. But it's worth knowing that's where that batch of errors is going to land.
What you can't conclude from this alone
This analysis is based on the official documentation and well-known TypeScript patterns. What you can't infer from here:
- How many errors it'll generate in your specific codebase. You only know that by running
tsc --noEmitwith each flag enabled. - Whether
exactOptionalPropertyTypesis worth the cost in a project with Prisma v5 and no prior refactors. It can be a lot of work for marginal value if the data model is already well-typed another way. - Whether there are incompatibilities with third-party libraries that don't handle
noUncheckedIndexedAccesswell.skipLibCheck: truemitigates this but doesn't eliminate it.
Deciding when to enable each flag requires running the compiler on your own code and reading the errors. No shortcuts here.
FAQ
Does strict: true enable noUncheckedIndexedAccess?
No. strict: true is a preset that enables eight specific flags documented in the official reference. noUncheckedIndexedAccess is not one of them. You have to enable it separately in tsconfig.json.
Which flag should I enable first if my project has none of them?
strictNullChecks. It's the one that prevents the largest class of runtime errors and is the logical prerequisite for the other flags to make sense. Without null checks, the rest is decoration.
Does noImplicitAny break explicit any usage?
No. noImplicitAny only penalizes the any TypeScript infers when it can't determine the type. If you write explicit any (const x: any = ...), it still compiles. That's intentional: sometimes you need to escape the type system. But at least you're doing it consciously.
Can I enable these flags progressively in a monorepo?
Yes. Each package in the monorepo can have its own tsconfig.json that extends a shared base. A common strategy is to enable the stricter flags in new packages and migrate the old ones incrementally. The risk is that types crossing package boundaries can land in gray zones during the transition.
Does exactOptionalPropertyTypes break object spreads?
It can, if you're using spreads to pass optional properties with value undefined. The compiler will flag those cases because there's a semantic difference between an absent property and a property with value undefined. In most cases, the fix is to use narrowing or conditional spreads instead of assuming undefined passes through transparently.
Is it worth enabling all of this in a project that already works?
Depends on the cost of the bugs you're trying to prevent. If the system handles authentication, financial data, or any kind of information where a silent error has real consequences — yes, the migration cost is worth it. If it's an internal prototype that never reaches users — maybe strict: true is enough for now. The criterion is the cost of the error, not the comfort of the setup. This ties directly into broader architectural decisions, the kind that come up in posts like the one on .
The next concrete step: run tsc --noEmit with noUncheckedIndexedAccess: true on the project you're working on right now. Read the errors. If they're manageable, enable it. If there are 200+ errors, start with the most critical files. You don't need to fix everything at once — you need to know what you've been ignoring.
Original sources:
- TypeScript Strict Mode Docs:
This article was originally published on juanchi.dev
SOCIAL SHARE CARD GENERATOR