TypeScript Strict Mode: Why Enable It and How to Fix Every Error
By Eoin Villarin, 2026-03-28, updated 2026-10-28
TypeScript 6 and 7 now turn strict mode on by default. Learn what each strict flag catches, how to migrate a large codebase step by step and how to fix common errors fast.
Should I enable TypeScript strict mode, and how do I fix all the errors?
Yes, enable TypeScript strict mode: it turns null, implicit any and unsafe catch-variable bugs into compile errors, and TypeScript 6.0 made it the default in March 2026. For large codebases, migrate flag by flag, starting with noImplicitAny and then strictNullChecks, or file by file with a second tsconfig, and enforce it with tsc --noEmit in CI.
Key takeaways
- TypeScript 6.0, released in March 2026, made strict default to true, and TypeScript 7.0 kept that default.
- TypeScript's strict option currently enables nine flags, including strictNullChecks, noImplicitAny and useUnknownInCatchVariables.
- Migrate flag by flag: set strict false explicitly, enable noImplicitAny first, then strictNullChecks, then the rest.
- Prefer @ts-expect-error over @ts-ignore, because it fails once the underlying error is fixed.
- Validate external API, form and webhook data at the boundary with a Zod schema so types cannot drift.
TypeScript strict mode is the difference between a type checker that politely watches and one that actually stops bugs. With strict checks off, TypeScript lets undefined flow into places that crash at runtime, lets untyped parameters quietly become any, and treats every caught error as safe to read. Those are the exact bugs that show up as "Cannot read properties of undefined" in production logs.
In 2026 the question has also changed. TypeScript 6.0 turned strict on by default, and TypeScript 7.0 kept that default. Upgrading an older project now surfaces hundreds of errors at once, and the temptation is to set "strict": false and move on.
This guide explains what each strict flag does, which ones catch the most real bugs, how to migrate a large codebase without freezing feature work, and the code patterns that fix the most common errors quickly.
Is TypeScript strict mode on by default now?
Yes. The TypeScript 6.0 release in March 2026 made strict default to true, alongside other new defaults such as module: esnext, an empty types array and a rootDir fixed to the folder containing tsconfig.json. Projects that relied on the old default now have to write "strict": false explicitly to keep the lenient behavior.
Then the TypeScript 7.0 release in July 2026 shipped the Go-based native compiler under the normal typescript package and tsc command, with full builds typically 8 to 12 times faster according to Microsoft. It behaves like 6.0 on existing code, including the strict default. Faster type checking also removes the old excuse that strict mode makes CI too slow.
- New projects: you get strict mode automatically. Leave it on.
- Projects with
"strict": truealready: nothing changes except new flags that future versions may add to the strict family. - Older projects with no
strictsetting: upgrading flips you into strict mode overnight. Use the migration plan below instead of switching it off.
If you also run TypeScript files directly with Node.js type stripping, strict typing matters even more, because Node removes types without checking them. I cover that workflow in my Node.js 22 features guide.
What does strict: true actually enable?
The strict option is a shorthand for a family of checks. According to the official tsconfig reference, it currently turns on nine flags, and you can still disable any of them individually.
| Flag | What it catches | Typical fix |
|---|---|---|
strictNullChecks | Using values that may be null or undefined | Optional chaining, ??, narrowing |
noImplicitAny | Parameters and variables silently typed as any | Add annotations or better inference |
useUnknownInCatchVariables | Reading properties of caught errors without checks | instanceof Error guard |
strictFunctionTypes | Unsafe function parameter assignments | Correct callback signatures |
strictBindCallApply | Wrong arguments to bind, call, apply | Fix argument lists |
strictPropertyInitialization | Class fields never assigned in the constructor | Initialize, make optional or use ! |
noImplicitThis | this with an implicit any type | Arrow functions or a this parameter |
alwaysStrict | Emits JavaScript strict mode in every file | Usually nothing |
strictBuiltinIteratorReturn | Built-in iterator results typed as any | Handle the undefined return value |
Which strict flags catch the most real bugs?
Not all nine flags are equal. Three of them account for most of the errors you will fix, and most of the production crashes they prevent.
strictNullChecks
Without it, null and undefined are assignable to every type, so the compiler cannot warn you when an API returns nothing. With it, a value typed User | undefined must be checked before you read user.email. Teams that turn it on for the first time usually find a long list of real crashes waiting to happen, especially around array lookups, optional API fields and DOM queries.
noImplicitAny
Every implicit any is a hole where type checking stops. A parameter like function total(items) accepts anything, and every call site loses its safety. This flag forces an annotation wherever TypeScript cannot infer a type, which also makes code easier for the next developer to read.
useUnknownInCatchVariables
JavaScript can throw anything: strings, objects, undefined. This flag types the catch variable as unknown, so you must check what it is before reading err.message. It is a small change that makes error handling and logging far more reliable.
How to migrate a large codebase to TypeScript strict mode
Turning on every flag in one commit works for small projects. For anything with more than a few hundred errors, you need a plan that lets feature work continue while the error count goes down. I treat the error count as a metric and track it weekly.
Option A: flag by flag
- Set
"strict": falseexplicitly so the TypeScript 6 or 7 upgrade does not flood the build. - Enable
noImplicitAnyfirst. Its errors are mechanical and fast to fix, and better annotations make the next step easier. - Enable
strictNullChecks. This is the big one, so split the work by folder or feature. - Enable
useUnknownInCatchVariablesandstrictPropertyInitialization. - Remove the individual flags and set
"strict": true.
{
"compilerOptions": {
"strict": false,
"noImplicitAny": true,
"strictNullChecks": true
}
}
Option B: file by file with a second tsconfig
Create a tsconfig.strict.json that extends your main config, turns strict on and lists only the files you have migrated. Run it in CI next to the normal check. New files go in the strict list from day one, and the list grows until it covers the whole project.
{
"extends": "./tsconfig.json",
"compilerOptions": { "strict": true, "noEmit": true },
"include": ["src/lib/**/*", "src/features/checkout/**/*"]
}
Some teams use the community typescript-strict-plugin instead, which marks unmigrated files with a //@ts-strict-ignore comment. It works, but tools that depend on the compiler API need TypeScript 6 until the new API lands in TypeScript 7.1, so check compatibility before you upgrade. The second tsconfig approach has no such dependency.
How to fix the most common strict mode errors
Most strict errors fall into a handful of shapes. Learn these patterns and you will clear the bulk of them quickly.
"Object is possibly undefined"
type User = { id: string; profile?: { displayName?: string } }
function greet(user: User | undefined) {
// Error: 'user' is possibly 'undefined'.
// return `Hello, ${user.profile.displayName}`
const name = user?.profile?.displayName ?? 'there'
return `Hello, ${name}`
}
Optional chaining (?.) stops at the first missing value, and nullish coalescing (??) supplies a default only for null or undefined, unlike ||, which also replaces 0 and empty strings.
Narrowing with type guards
function isDefined<T>(value: T | null | undefined): value is T {
return value != null
}
const skus = cartLines.map((line) => line.sku).filter(isDefined)
Since TypeScript 5.5, simple callbacks such as .filter((x) => x !== undefined) infer the type predicate automatically, so you often do not need the helper at all.
Errors in catch blocks
try {
await saveOrder(order)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
logger.error({ message, orderId: order.id })
}
Uninitialized class properties
Initialize the field, make it optional, or assign it in the constructor. Use the definite assignment assertion (private client!: ApiClient) only when a framework sets the value for you, and leave a comment saying which one.
The non-null assertion operator (!) is a promise to the compiler, not a fix. Every one you add is a place where strict mode can no longer protect you, so keep them rare and searchable.
How to validate external data at the boundary
Strict mode only guarantees what TypeScript can see. Data from an API, a form, a webhook or localStorage arrives as unknown at runtime, whatever your types claim. Parse it once at the edge of your app and the rest of your code can trust the types.
import * as z from 'zod'
const Order = z.object({
id: z.string(),
email: z.email(),
total: z.number().nonnegative(),
notes: z.string().optional(),
})
type Order = z.infer<typeof Order>
const result = Order.safeParse(await res.json())
if (!result.success) throw new Error('Invalid order payload')
const order: Order = result.data
This pairs naturally with strict mode, because the schema and the type can never drift apart. I cover the full pattern, including shared schemas between client and server, in my guide to type-safe APIs with TypeScript and Zod.
How to keep strict mode from regressing
Once the error count reaches zero, protect it. Strict mode erodes quietly through as any casts and blanket @ts-ignore comments unless something stops them.
- Type check in CI: run
tsc --noEmiton every pull request and fail the build on any error. - Lint the escape hatches: typescript-eslint rules like
no-explicit-anyandban-ts-comment(with required descriptions) make every exception visible in review. - Prefer
@ts-expect-error: unlike@ts-ignore, it fails once the underlying error is fixed, so stale suppressions get cleaned up. - Go further when ready:
noUncheckedIndexedAccessandexactOptionalPropertyTypesare not part ofstrict, but they catch array index and optional property bugs that strict mode misses.
Coding agents are good at the mechanical part of this work, such as adding annotations and null checks across hundreds of files. I explain how I review that output safely in my post on using AI coding agents on client projects.
Frequently asked questions
Should I set strict to false after upgrading to TypeScript 6 or 7?
Only as a temporary step. Setting "strict": false explicitly keeps an old project building after the upgrade, but you should then enable individual flags, starting with noImplicitAny and strictNullChecks, until you can switch strict back on. Leaving it off permanently hides the bugs the new default was meant to catch.
Does TypeScript strict mode slow down compilation?
Not in any way you will notice. Strict checks add a small amount of analysis, but the bigger factor is the compiler itself, and TypeScript 7.0's native compiler is reported to be 8 to 12 times faster on full builds than the JavaScript-based one. Strict mode costs far less time than debugging the null errors it prevents.
Is it safe to use the non-null assertion operator?
It is safe only when you know something the compiler cannot, such as a value a framework assigns before use. Each ! disables strict null checking at that point, so a wrong assumption becomes a runtime crash. Prefer narrowing, defaults or runtime validation, and keep remaining assertions rare and commented.
Does strict mode affect the JavaScript output?
Almost entirely no. Strict mode changes type checking, not emitted code. The one exception is alwaysStrict, which adds the "use strict" directive to non-module files. ES modules are already strict, so for most modern projects the output is identical with or without strict mode.
Make strict mode your default
TypeScript strict mode is now the default for good reason: it turns whole classes of runtime crashes into compile errors you fix once. Migrate flag by flag or file by file, fix the common patterns with optional chaining, narrowing and guarded catch blocks, validate data at the boundary, and lock the result in with CI.
If you have a large codebase that needs to move to TypeScript 7 and strict mode without stalling your roadmap, send me a message through my contact form. You can see the kind of projects I build and maintain in my case studies.