TypeScript Strict Mode: The tsconfig Flags That Actually Matter
A practical breakdown of TypeScript strict mode — what each flag does, which ones strict enables, and what strict misses that you should add manually.
"strict": true is the most common TypeScript configuration advice. Enable it, and TypeScript helps you more. But what does strict actually do, which bugs does it catch, and what does it miss? Most developers can answer the first question vaguely and not at all for the last two.
This guide breaks down each flag that strict enables and covers what strict does not enable — where you need to add flags explicitly to close real gaps.
What TypeScript Strict Mode Enables
strict: true in your tsconfig.json is shorthand for eight specific compiler flags. Enabling strict is equivalent to setting all of them individually.
strictNullChecks
The most impactful flag. Without it, null and undefined are assignable to every type. With it, they are their own distinct types:
// Without strictNullChecks
let name: string = null; // allowed
// With strictNullChecks
let name: string = null; // Error: Type 'null' is not assignable to type 'string'
let name: string | null = null; // correctThis catches the largest class of runtime errors — accessing a property on something that turned out to be null or undefined. If you only enable one flag, make it this one. strict: true includes it.
noImplicitAny
Without this flag, TypeScript silently infers any when it cannot determine a type — particularly for function parameters:
// Without noImplicitAny — TypeScript infers any for 'user'
function log(user) {
console.log(user.name); // no error even if name does not exist
}
// With noImplicitAny
function log(user: User) { // must annotate explicitly
console.log(user.name);
}any disables type checking for that value. Implicit any is TypeScript silently giving up without telling you. This flag makes silence impossible — TypeScript either infers a real type or requires an explicit annotation.
strictFunctionTypes
Makes function parameter types checked contravariantly. Without this, you can pass a function expecting a Dog where a function expecting an Animal is required, which allows type-unsafe code. With it, the check is strict in both directions.
This flag primarily affects higher-order functions and callbacks. Most developers do not notice it directly, but it closes a real soundness hole.
strictBindCallApply
Makes bind, call, and apply type-checked. Without it, these methods accept any arguments and return any. With it, TypeScript verifies that the arguments you pass to fn.call(thisArg, arg1, arg2) match fn's parameter types.
strictPropertyInitialization
Requires that class properties are either initialized in the declaration or assigned in the constructor:
class UserService {
private db: Database; // Error: Property 'db' has no initializer
// Fix: assign in constructor
constructor(db: Database) {
this.db = db;
}
}This catches the common pattern of declaring a class property but forgetting to initialize it, which would cause a runtime error the first time the property is accessed.
noImplicitThis
Requires explicit typing of this in functions where TypeScript cannot determine its type automatically. Relevant mostly in class methods, event handlers, and functions passed as callbacks.
alwaysStrict
Emits "use strict" at the top of every compiled JavaScript file. This is largely cosmetic in modern Node.js (which runs in strict mode by default) but ensures correct behavior in older environments.
useUnknownInCatchVariables
Since TypeScript 4.4, catch clause variables default to unknown with this flag instead of any. This is correct — you do not know what was thrown:
try {
riskyOperation();
} catch (err) {
// With this flag: err is unknown
// Without: err is any (TypeScript lets you do anything with it)
if (err instanceof Error) {
console.error(err.message);
}
}TypeScript Strict Mode: What It Misses
strict: true does not cover everything. These flags provide meaningful additional safety and need to be added separately.
noUncheckedIndexedAccess
The most impactful flag not in strict. Without it, array index access returns T, not T | undefined:
const items: string[] = [];
const first = items[0]; // string — TypeScript lies
first.toUpperCase(); // runtime crash
// With noUncheckedIndexedAccess:
const first = items[0]; // string | undefined — TypeScript is honestThis catches a very common class of runtime crash. The fix is usually a nullish coalescing or an explicit guard. Enable it, fix the errors, ship with confidence.
noImplicitReturns
Catches functions that fall through without returning a value in some branches:
function getLabel(status: string): string {
if (status === "active") return "Active";
// forgot to handle other cases — implicit return is undefined
}With noImplicitReturns, the function above is a compile error. Add it.
exactOptionalPropertyTypes
Without this flag, setting an optional property to undefined is the same as having it present with an undefined value — which is semantically different from omitting it:
interface Config {
timeout?: number;
}
const cfg: Config = { timeout: undefined }; // allowed without exactOptionalPropertyTypesWith it, the above is an error. You must either omit timeout or set it to a number. This matters when serializing to JSON or when the receiving code checks "timeout" in cfg.
The Recommended Production Combination
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true
}
}Start here. These flags together close the most common gaps between "TypeScript says it is fine" and "this crashes in production."
If your team wants to get TypeScript configuration right from the start — or tighten up an existing codebase — Clixo can audit your setup and help you ship with higher confidence.