EmpireUI
Get Pro
← Blog7 min read#typescript#strict-mode#type-safety

TypeScript Strict Mode: Every Flag and What It Catches

TypeScript strict mode isn't just one flag — it's eight. Here's exactly what each one catches, when it fires, and whether you actually need it in your project.

Code editor showing TypeScript type errors highlighted in red on a dark background

What strict: true Actually Enables in tsconfig.json

Honestly, most developers think strict: true is a single toggle. It's not. In TypeScript 5.x, setting strict: true in your tsconfig.json is shorthand for enabling eight distinct compiler flags simultaneously — and each one catches a different class of bug.

The eight flags are: strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitAny, noImplicitThis, alwaysStrict, and useUnknownInCatchVariables. You can enable strict mode and then individually turn one off if it's causing too much noise in a migration. That's a legitimate approach.

If you're starting a greenfield project in 2026 — whether that's a Next.js 15 app, a Vite setup, or a component library — just enable all eight from day one. The cost of retrofitting strict mode onto an existing codebase is real. The cost of skipping it is usually worse.

strictNullChecks: The Flag That Changes Everything

strictNullChecks is the one that causes the most breakage during migration, and also the one that prevents the most runtime crashes. Without it, null and undefined are assignable to every type. string and string | null | undefined are the same thing. TypeScript lets it slide.

With it enabled, the compiler tracks nullability through the type graph. If a function returns User | null and you call .name on the result without checking for null first, you get a compile error. Not a runtime crash at 2am. A compile error at your desk.

The practical benefit shows up most in data-fetching code. API responses can be absent. DOM queries can return null. Event handlers can fire before state is ready. strictNullChecks forces you to handle all of that explicitly, which is annoying for about three days and then becomes second nature.

noImplicitAny and noImplicitThis Explained

noImplicitAny rejects any variable or parameter whose type can't be inferred and hasn't been annotated. Without it, TypeScript silently assigns the any type when it can't figure things out — which defeats a large part of the point. With it on, you're forced to be explicit.

The flag catches a specific anti-pattern: function parameters that get typed as any because you forgot to annotate them. This matters when you're building utility functions, hooks, or component props that get reused across a codebase. If you're building something like a component library — say, one of the 40 style variants in Empire UI — an implicit any in a shared prop interface will silently swallow type errors from every consumer.

noImplicitThis is narrower. It fires when you use this inside a function where TypeScript can't determine what this refers to. This comes up most often with class methods passed as callbacks, and with older jQuery-style patterns. In React function component codebases it's basically a non-issue, but if you're maintaining legacy code or writing vanilla DOM utilities, it'll save you.

strictFunctionTypes and Contravariance in Callbacks

This is the one that trips up experienced TypeScript developers. strictFunctionTypes enables contravariant checking for function parameter types. In plain terms: it determines whether a (dog: Dog) => void callback is assignable to an (animal: Animal) => void slot.

Without this flag, TypeScript uses bivariant checking for method definitions (for historical reasons tied to how class methods work). With it on, function type parameters are checked contravariantly. This is technically correct — if a callback promises to accept any Animal, passing it a function that only handles Dog is unsafe. The flag enforces that.

In practice you'll feel this most with event handler types and array method callbacks. Here's a concrete example of what it catches: ``tsx type Logger = (message: string | number) => void; // Without strictFunctionTypes, this would compile fine. // With it on, this correctly errors — the callback only handles string, // but the type contract says it must handle string | number. const logStringOnly = (message: string) => console.log(message); const logger: Logger = logStringOnly; // Error with strictFunctionTypes `` If you're wiring up event systems, context APIs, or anything with callback contracts, this flag will catch real bugs before they ship.

strictPropertyInitialization and Class Fields

strictPropertyInitialization requires that every declared class property is either assigned in the constructor or marked as possibly undefined. This matters if you're using classes — service classes, store classes, custom error types, anything built with class syntax.

The most common pattern it rejects is declaring a class property without initializing it: ``tsx class UserService { // Error: Property 'apiClient' has no initializer and // is not definitely assigned in the constructor. private apiClient: ApiClient; // Fix option 1: assign in constructor constructor(client: ApiClient) { this.apiClient = client; } // Fix option 2: mark as definitely assigned with ! // (use sparingly — you're opting out of the check) private otherClient!: ApiClient; } ` The definite assignment assertion (!`) is a deliberate escape hatch, not a workaround you should reach for by default. Use it when a property really is initialized through some mechanism TypeScript can't see — like a setup method called by a DI framework.

useUnknownInCatchVariables: The TypeScript 4.4 Addition

Added in TypeScript 4.4 and included in strict: true since then, useUnknownInCatchVariables changes the type of e in a catch block from any to unknown. It's a small change with a real impact on error handling quality.

When e is any, you can call e.message or e.statusCode without any checks and TypeScript won't complain. If the thrown value is actually a string, or a plain object, or undefined, you get a runtime error. With unknown, you're forced to narrow the type before using it — typically with instanceof Error or a type guard.

Is it annoying to update every try-catch in a legacy codebase? Yes. Does it produce better error handling code? Also yes. When you're debugging production issues in a Next.js app, knowing exactly what shape your caught errors are makes a real difference.

Migrating an Existing Codebase to Strict Mode

You can't just flip strict: true in an existing project and expect to compile. The approach that works is incremental. TypeScript lets you enable flags one at a time — start with noImplicitAny, fix the errors, commit. Then add strictNullChecks. Repeat. Most teams do this over two to four weeks depending on codebase size.

There's also a skipLibCheck: true option that ignores errors in node_modules type declarations. Keep that on during migration — some third-party packages have declaration files with errors that aren't your problem to fix. Focus on your own code first.

For component libraries specifically, the order matters. Fix noImplicitAny first because it has the widest surface area. Then strictNullChecks because it interacts with every prop type and return value. strictPropertyInitialization and strictFunctionTypes usually produce the fewest errors and can go last. If you're building with Empire UI components as a base, they already compile clean under strict mode, so you won't inherit errors from the component layer.

Which Strict Flags Are Worth Enabling Beyond the Bundle

There are useful strictness-adjacent flags that aren't included in strict: true and don't get enough attention. noUncheckedIndexedAccess is the most impactful one. It adds | undefined to the result of any array index access (arr[0]) or object index signature access. Without it, TypeScript assumes arr[0] always returns a valid element even when the array might be empty.

exactOptionalPropertyTypes is another one. By default, { foo?: string } allows you to set foo to undefined explicitly, even though ? only means the property can be absent. With exactOptionalPropertyTypes: true, setting an optional prop to undefined explicitly becomes an error. It's a subtle distinction but it matters for serialization and API contract work.

How aggressively you go depends on your team. noUncheckedIndexedAccess in particular generates a lot of | undefined narrowing that can feel excessive if your array iteration patterns are already defensive. But if you care about theme toggling logic or any code that reads from config objects by key, it'll catch access patterns you'd never think to test manually.

FAQ

Does enabling strict: true break existing TypeScript code?

Almost certainly yes, in any codebase that wasn't written with strict mode in mind. The number of errors depends heavily on how disciplined the original code was. Projects with typed function parameters and null checks throughout will have fewer errors. Codebases with lots of implicit any or unchecked DOM access will have many more. Enable flags incrementally to manage the migration load.

Can I enable strict mode but disable one specific flag?

Yes. Set strict: true and then explicitly set the flag you want to disable to false. For example: { "strict": true, "strictPropertyInitialization": false }. The explicit false overrides the strict bundle. This is useful during incremental migrations where you want most strict checks but not all at once.

Is useUnknownInCatchVariables included in TypeScript 5.x strict mode?

Yes. It was added in TypeScript 4.4 and has been part of the strict bundle ever since. In TypeScript 5.x (the current major as of 2026), it's fully included. Catch block variables are typed as unknown, not any, when strict or useUnknownInCatchVariables is true.

What's the difference between noImplicitAny and strict: true for a new project?

noImplicitAny is one of the eight flags that strict: true enables. Setting strict: true gives you all eight simultaneously. For new projects there's no reason to pick and choose — just set strict: true in tsconfig.json and you get the full set. The only time to enable flags individually is during a migration of existing code where you need to fix errors in batches.

Does strictFunctionTypes affect method definitions on interfaces and classes?

No, and this is intentional. strictFunctionTypes only applies to function types written in function syntax (using =>). Methods written in method syntax (using the shorthand foo(arg: T): R form) still use bivariant checking for backwards compatibility. This means the flag doesn't catch all variance issues, just the ones in function-typed properties and parameters.

Should I use noUncheckedIndexedAccess in a React component library?

It depends. noUncheckedIndexedAccess adds | undefined to every array index access, which means children[0] returns ReactNode | undefined instead of ReactNode. If your components iterate over arrays with map() you won't notice it. If you access array elements by index in prop processing logic, you'll need more null checks. Worth enabling and seeing how many errors appear — often it's fewer than expected.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

TypeScript vs JavaScript for React UI: The Case for Strict ModeReact Native vs Expo: Which to Use for Cross-Platform in 2026React Hook Form + Zod: Type-Safe Forms with ValidationBest VS Code Extensions for React Developers in 2026