Is strict TypeScript worth it? A practical take
What each strict TypeScript flag actually does, where the friction is real, and how to turn strict mode on without a week of red squiggles.

Someone always asks this on day one of a project: do we turn on strict, or do we keep things loose so we can move fast? The honest answer is that strict mode is worth it on almost every TypeScript codebase, but not for the reasons people usually give. It is not about purity. It is about which bugs you find at compile time versus 2am in production.
What "strict" actually turns on
"strict": true in your tsconfig.json is not one setting. It is a bundle of flags, and the two that earn their keep are strictNullChecks and noImplicitAny. The rest matter less day to day.
Without strictNullChecks, null and undefined are assignable to every type. That sounds convenient until you remember that the single most common runtime error in JavaScript is reading a property off something that turned out to be undefined. Here is the difference in practice:
type User = { id: string; name: string; email?: string };
function greeting(user: User): string {
// Without strictNullChecks, this compiles and crashes at runtime
// when email is missing. With it on, TS forces you to handle the gap.
return `Hi ${user.name}, we'll reach you at ${user.email.toLowerCase()}`;
// ^ 'user.email' is possibly 'undefined'
}With strict null checks on, that line does not compile. You are forced to write the branch you would have written anyway after the bug report:
function greeting(user: User): string {
const email = user.email?.toLowerCase() ?? "no email on file";
return `Hi ${user.name}, we'll reach you at ${email}`;
}noImplicitAny is the other one that pays for itself. It flags any parameter or variable that silently became any because TypeScript could not infer a type. Silent any is how a fully typed codebase quietly rots: one untyped function argument poisons everything downstream, and you stop noticing because the editor stops complaining.
Where the friction is real
We are not going to pretend strict mode is free. There are two places it genuinely costs you time, and it helps to name them so you do not get blindsided.
Third-party data at the boundary
TypeScript checks your code, not the JSON coming back from an API. The moment you await res.json(), you have an any, and strict mode will not save you because you told it the shape yourself. This is the real lesson: strict mode pushes the danger to your system boundaries, which is exactly where you want it. Validate there with something like Zod and the types flow inward correctly:
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email().optional(),
});
async function fetchUser(id: string): Promise<z.infer<typeof UserSchema>> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`User fetch failed: ${res.status}`);
// parse() throws on bad shape, so everything after this point is truly typed
return UserSchema.parse(await res.json());
}Without this, strict mode gives you a false sense of safety. Your code is sound, but garbage from the network walks right past the type checker. Validating at the edge is the pattern that makes strict TypeScript actually mean something.
Array and object access
The flag people fight hardest is noUncheckedIndexedAccess, which is not even part of strict by default. It makes arr[0] return T | undefined instead of T, because indexing past the end of an array is a runtime undefined and TypeScript was lying to you about it before. It is correct, and it is also annoying in tight loops where you know the index is valid. Our take: turn it on for new projects, leave it off when retrofitting a large one, because the noise-to-value ratio is rough on existing code.
Turning it on without a bad week
The mistake teams make is flipping strict to true on a 200-file codebase and staring at 4,000 errors. That is how strict mode gets reverted by Friday. Do it incrementally instead.
Start by enabling the individual flags one at a time rather than the whole bundle. Order matters: noImplicitAny first, then strictNullChecks, since the null errors are the ones that take real thought.
For an existing codebase, the cleanest path is a per-directory rollout using TypeScript project references or a tool like ts-strictify, but you can get most of the value with a simpler move. Enable strict globally, then suppress the existing failures so only new code is held to the standard:
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}Then run the compiler and let it annotate every current failure with // @ts-expect-error comments via a codemod. New code starts clean, and you burn down the old errors as you touch each file. The key property of @ts-expect-error over @ts-ignore: if you later fix the underlying issue, the suppression comment itself becomes an error, so your codebase tells you when a workaround is no longer needed. It cleans up after itself.
One more practical note. If you use Next.js, strict mode in tsconfig.json is separate from the build failing on type errors. By default next build will refuse to ship with type errors, which is what you want. Resist the temptation to set typescript.ignoreBuildErrors: true just to unblock a deploy. That flag is how a "strict" project ships broken types every single release, and nobody notices until the types are decorative.
So, is it worth it?
For a greenfield project, yes, without hesitation. Turn on strict and noUncheckedIndexedAccess from the first commit, validate at your boundaries with a schema library, and you will spend your debugging time on actual logic instead of cannot read properties of undefined.
For a large legacy codebase, it is still worth it, but the answer is "yes, incrementally." Flip the flags one at a time, suppress the backlog, and hold new code to the standard. The teams that regret strict mode are almost always the ones that tried to do it all in one pull request.
The real value is not stricter types for their own sake. It is that strict mode forces you to decide, at compile time, what happens when data is missing or malformed. You were going to make those decisions anyway. Strict TypeScript just makes you make them before a user does.
If you want a second pair of eyes on a migration plan for an existing codebase, that is the kind of unglamorous work we genuinely enjoy.
