Strict mode enables eight compiler flags that catch null dereferences, implicit any, unsafe function types, and uninitialized class properties.
The two most impactful flags: strictNullChecks and noImplicitAny — they catch ~90% of real-world type bugs.
For greenfield projects, enable strict:true from the first commit. For legacy codebases, enable it globally, suppress with @ts-nocheck, then fix files incrementally.
Performance impact: zero runtime cost. Strict mode is a compile-time guard that emits identical JavaScript.
Production insight: A missing strictNullChecks flag caused an e-commerce checkout crash — the code compiled fine but crashed when a user profile was null.
✦ Definition~90s read
What is Strict Mode in TypeScript?
TypeScript's strict mode (strict: true in tsconfig.json) is a meta-flag that enables a suite of individual compiler checks designed to eliminate entire categories of runtime errors at compile time. It's not a single feature but a bundle of flags — strictNullChecks, noImplicitAny, strictPropertyInitialization, strictFunctionTypes, strictBindCallApply, and useUnknownInCatchVariables — each closing a specific type-safety hole.
★
Imagine you're writing a letter and you hire two proofreaders.
Without strict mode, TypeScript defaults to a permissive mode that tolerates null/undefined assignments to any type, allows implicit any for untyped variables, and skips variance checks on function parameters. This lax behavior was pragmatic for early adoption from JavaScript but creates the exact null crashes and type leaks strict mode exists to prevent.
In practice, strictNullChecks is the most impactful flag: it forces you to handle null and undefined explicitly, turning string | null into a distinct type from string. This alone eliminates the infamous "Cannot read property of undefined" crash that plagues JavaScript. noImplicitAny catches the silent any that creeps in when TypeScript can't infer a type — a common source of runtime surprises, especially for developers new to the type system. strictPropertyInitialization ensures class properties are either initialized in the constructor or marked with ?/!, preventing uninitialized property access. useUnknownInCatchVariables changes catch(e) from any to unknown, forcing you to narrow the error type before using it — a pattern that catches real-world bugs where error objects don't have the expected shape.
Strict mode is the default for new TypeScript projects, and for good reason: it's the difference between TypeScript being a glorified linter and a genuinely sound type system. Migrating a legacy project to strict mode requires incremental effort — typically starting with noImplicitAny and strictNullChecks on a per-file basis using // @ts-check or skipLibCheck for third-party types — but the payoff is measurable.
Companies like Airbnb, Lyft, and Slack have reported 30-50% reductions in production null-related bugs after enabling strict mode. The alternative is a codebase where null and undefined silently propagate through the type system, only to surface as runtime crashes in production — the exact problem TypeScript was designed to solve.
Plain-English First
Imagine you're writing a letter and you hire two proofreaders. The first one only flags spelling mistakes. The second one flags spelling, grammar, missing words, ambiguous sentences, AND tells you when something you wrote could be misunderstood. TypeScript's strict mode is that second proofreader — it doesn't just check the basics, it checks everything that could quietly cause a problem later. Without it, TypeScript is polite. With it, TypeScript is honest.
Most TypeScript developers enable strict mode on day one and never think about it again — and that's exactly the problem. When something eventually breaks because of an implicit any or an unchecked null, they have no idea why their 'typed' code behaved exactly like untyped JavaScript. Strict mode isn't just a setting you toggle; it's a philosophy about how seriously you take type safety. It's the difference between TypeScript as a linter and TypeScript as a genuine safety net.
TypeScript was designed to be gradually adoptable, which means its default settings are deliberately lenient. You can migrate a giant JavaScript codebase to TypeScript without changing a single line of logic — which sounds great until you realise you've just added TypeScript syntax without TypeScript's most valuable protections. The problems strict mode prevents — null dereferences, unintended any leakage, implicit function return types — are precisely the bugs that take hours to track down in production.
By the end of this article you'll understand exactly what each flag inside strict mode does, why each one was created, and how to use them in a real project. You'll be able to read a tsconfig.json and immediately know how protected that codebase really is — which is a skill that genuinely separates intermediate TypeScript developers from advanced ones.
What 'strict: true' Actually Enables Under the Hood
Setting 'strict: true' in your tsconfig.json is a shorthand. It doesn't flip one switch — it flips eight. Understanding each one individually is critical because when you migrate a legacy project you'll often need to turn them on one at a time, and when an error appears you need to know which rule it's coming from.
The eight flags that 'strict: true' enables are: strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitAny, noImplicitThis, alwaysStrict, and useUnknownInCatchVariables (added in TypeScript 4.4).
Each one was added in response to a real category of bugs that people kept shipping. They're not academic — they're battle scars. The two you'll feel immediately are noImplicitAny and strictNullChecks. Those two alone will surface more latent bugs in a typical codebase than everything else combined.
You can verify which flags are active at any time by running 'tsc --showConfig' in your terminal. It prints the fully resolved config including all inherited defaults, so there's no ambiguity about what's actually running.
tsconfig.jsonJSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
"compilerOptions": {
// Enabling'strict' is equivalent to enabling ALL eight flags below.
// We're spelling them out here so you can see exactly what you're getting.
"strict": true,
// --- What'strict: true' implicitly sets ---
// 1. null and undefined are NOT assignable to other types.
// This is the single most impactful flag.
"strictNullChecks": true,
// 2. Variables can't silently become 'any' when TypeScript can't infer a type.
"noImplicitAny": true,
// 3. 'this' in functions must have an explicit type — prevents confusing runtime errors.
"noImplicitThis": true,
// 4. Class properties must be assigned in the constructor (or declared with '!').
"strictPropertyInitialization": true,
// 5. Function parameter types are checked contravariantly (prevents subtle type holes).
"strictFunctionTypes": true,
// 6. .bind(), .call(), .apply() are type-checked against the original function signature.
"strictBindCallApply": true,
// 7. EveryJS file gets 'use strict' emitted at the top automatically.
"alwaysStrict": true,
// 8. Catch clause variables are typed as 'unknown' instead of 'any' (TS4.4+).
"useUnknownInCatchVariables": true,
// Recommendedcompanions (not part of 'strict' but work well with it)
"noUncheckedIndexedAccess": true, // array[index] returns T | undefined, not just T
"exactOptionalPropertyTypes": true // { name?: string } means string, NOT string | undefined
}
}
Output
No runtime output — this is a config file. Run 'tsc --showConfig' to verify the resolved settings.
Pro Tip:
If you're enabling strict on a legacy project, add 'strict: true' then immediately add '// @ts-nocheck' to the files that explode. Fix them one file at a time. Never try to fix the whole repo at once — you'll give up on day two.
Production Insight
A team at a fintech turned on strict:true across a 300-file repo without @ts-nocheck. They spent three weeks fixing errors and shipped no features.
The lesson: incremental migration prevents burnout. Use the suppression-first strategy.
If you don't see errors in your IDE but CI fails, check which tsconfig your build script uses — it might be a different file.
Key Takeaway
strict:true is shorthand for eight safety nets.
Know each flag by name so you can pinpoint errors in seconds.
The fastest migration path is: enable, suppress, fix incrementally.
Which strict flag do I need?
IfI'm removing null checks because they 'annoy me'
→
UseDo NOT disable strictNullChecks. Instead, use optional chaining and nullish coalescing. The annoyance is a feature.
IfI'm getting 'input' implicitly has an 'any' type
→
UseEnable noImplicitAny and annotate the parameter. If it's a callback, type it: (event: MouseEvent) => void
IfI'm inheriting a codebase and see many 'uninitialized property' errors
→
UseEnable strictPropertyInitialization and fix property assignments. Use '!' only for DI frameworks that set properties after construction.
thecodeforge.io
Strict Mode Null Crash Prevention
Strict Mode Typescript
strictNullChecks and noImplicitAny: The Two Flags That Change Everything
These two flags deserve their own section because they're responsible for the vast majority of bugs strict mode catches. Without strictNullChecks, null and undefined can be assigned to any variable of any type — which is exactly how JavaScript works, and exactly why JavaScript is so error-prone.
Consider a function that fetches a user from a database. Without strictNullChecks, you can type the return value as 'User' even though the function clearly might return null when no user is found. TypeScript won't complain. Every caller of that function then assumes they have a User object and calls .name or .email on it — until one day a user isn't found and the whole thing crashes at runtime.
noImplicitAny closes a different gap. TypeScript infers types from usage, but when it genuinely can't infer — like an untyped function parameter — it has two choices: error, or silently assign 'any'. Without noImplicitAny it chooses silence. That silence erases type safety from that point forward in your code, like a hole in a net.
These two flags work together. strictNullChecks narrows the universe of values a type can hold. noImplicitAny ensures that universe is actually defined. Both are essential.
UserLookup.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// --- WITHOUT strict mode (the dangerous default) ---// TypeScript doesn't complain here — but this function CAN return null.// Without strictNullChecks, TypeScript believes the return type is just 'User'.functionfindUserByIdUnsafe(userId: number) {
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
// Array.find() returns T | undefined — but without strictNullChecks,// TypeScript lets you treat the result as always being T. Danger.return users.find(user => user.id === userId);
}
const unsafeUser = findUserByIdUnsafe(99); // User with id 99 does NOT exist
console.log(unsafeUser.name); // RUNTIME CRASH: Cannot read properties of undefined// --- WITH strict mode (the safe, honest version) ---interfaceUser {
id: number;
name: string;
email: string;
}
// With strictNullChecks active, TypeScript correctly infers the return// type as 'User | undefined'. You're forced to handle both cases.functionfindUserById(userId: number): User | undefined {
const users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
return users.find(user => user.id === userId);
}
// TypeScript now REFUSES to compile this without a null check:// const brokenUser = findUserById(99);// console.log(brokenUser.name); // ERROR: Object is possibly 'undefined'// The correct pattern — narrow the type before using it:const safeUser = findUserById(1);
if (safeUser !== undefined) {
// TypeScript knows safeUser is type 'User' inside this block — not 'User | undefined'
console.log(`Found user: ${safeUser.name} (${safeUser.email})`);
} else {
console.log('No user found with that ID.');
}
// --- noImplicitAny in action ---// WITHOUT noImplicitAny, TypeScript silently types 'data' as 'any'.// Type safety is completely gone for this parameter and everything downstream.
function processDataUnsafe(data) { // TS would allow this without noImplicitAny
return data.toUpperCase(); // Could crash if 'data' is a number
}
// WITH noImplicitAny — you must declare the type. No more silent escape hatches.functionprocessData(data: string): string {
// TypeScript knows this is a string — .toUpperCase() is safe and autocomplete worksreturn data.toUpperCase();
}
console.log(processData('hello, world')); // Output: HELLO, WORLD
Output
Found user: Alice (alice@example.com)
HELLO, WORLD
(Without strict mode, line 10 would throw: TypeError: Cannot read properties of undefined (reading 'name'))
Watch Out:
Array.find(), Map.get(), and any DOM query (document.getElementById) all return T | undefined or null. These are the most common sources of null-related crashes. With strictNullChecks on, TypeScript forces you to handle the undefined case every single time. That's not inconvenient — that's the whole point.
Production Insight
A payment service crashed every Monday because the weekend cron job inserted user records with a null email field.
Without strictNullChecks, the service compiled fine. The crash was a runtime TypeError on user.email.toLowerCase().
Rule: always enable strictNullChecks and noImplicitAny before any other flag. They prevent the two most common classes of production TypeScript bugs.
Key Takeaway
strictNullChecks and noImplicitAny are the two highest-impact flags.
They prevent null dereferences and silent any leakage.
Always enable them first — they catch ~90% of real-world type bugs.
How to handle a possible null/undefined value?
IfThe value is from an API call or database query
→
UseUse optional chaining: user?.email and nullish coalescing: user?.email ?? 'fallback'. The type stays string | undefined.
IfYou're sure it's never null but TypeScript disagrees
→
UseUse a type assertion only as last resort: user!.email. Better: restructure the code to use a real guard.
IfThe value must be provided before use (e.g., set by a framework)
→
UseUse the definite assignment assertion ! on the property declaration, but ensure it's actually assigned before first use.
strictPropertyInitialization and useUnknownInCatchVariables in Practice
These two flags are less talked about but protect against genuinely nasty bugs.
strictPropertyInitialization ensures that every property declared in a class is actually assigned a value — either in its declaration or in the constructor. Without it, you can declare a property as 'string' but never assign it, and TypeScript won't warn you. At runtime, accessing that property returns undefined — which breaks your string assumption silently.
The fix is always one of three things: assign it inline, assign it in the constructor, or add the definite assignment assertion (!) if you genuinely know it'll be assigned before use (like by a dependency injection framework).
useUnknownInCatchVariables was added in TypeScript 4.4 and solves a real problem: error handling. Before this flag, catch clause variables were typed as 'any'. That meant you could write 'error.message' without TypeScript complaining — even if the thrown value was a string, a number, or some custom object with no .message property. With this flag, caught errors are typed as 'unknown', forcing you to check the type before using any properties. It's annoying for five minutes, then it saves you hours.
OrderService.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// ================================================================// strictPropertyInitialization — Class property safety// ================================================================classOrderService {
// Without strictPropertyInitialization, you could declare these and// forget to assign them. At runtime, they'd both be 'undefined'.private apiBaseUrl: string;
private maxRetries: number;
constructor(apiBaseUrl: string, maxRetries: number) {
// TypeScript verifies these assignments happen here.// Remove either line and you get: "Property has no initializer and// is not definitely assigned in the constructor."this.apiBaseUrl = apiBaseUrl;
this.maxRetries = maxRetries;
}
getOrderEndpoint(orderId: string): string {
// Safe — TypeScript knows apiBaseUrl is definitely a string herereturn `${this.apiBaseUrl}/orders/${orderId}`;
}
}
// A class that uses lazy initialization (e.g. set by a framework after construction)classReportGenerator {
// The '!' (definite assignment assertion) tells TypeScript:// "Trust me — this WILL be assigned before it's read."// Use this sparingly and only when you're 100% sure.private templateEngine!: { render: (template: string) => string };
// Called by a framework (e.g. Angular's ngOnInit) after constructioninitialize(engine: { render: (template: string) => string }): void {
this.templateEngine = engine;
}
generateReport(template: string): string {
returnthis.templateEngine.render(template);
}
}
// ================================================================// useUnknownInCatchVariables — Safe error handling// ================================================================interfaceApiError {
statusCode: number;
message: string;
}
// A helper that checks if a thrown value matches our ApiError shapefunctionisApiError(error: unknown): error is ApiError {
// We check every property before trusting the shapereturn (
typeof error === 'object' &&
error !== null &&
'statusCode'in error &&
'message'in error
);
}
asyncfunctionfetchOrderDetails(orderId: string): Promise<void> {
try {
// Simulating a fetch call that might throw
const response = await fetch(`https://api.example.com/orders/${orderId}`);if (!response.ok) {
// We throw a structured object, not just a stringthrow { statusCode: response.status, message: 'Order not found' } asApiError;
}
const order = await response.json();
console.log('Order details:', order);
} catch (error) {
// With useUnknownInCatchVariables, 'error' is typed as 'unknown'.// TypeScript REFUSES to compile: console.log(error.message)// because it doesn't know error has a .message property.if (isApiError(error)) {
// Inside this block TypeScript knows 'error' is ApiError
console.error(`APIError ${error.statusCode}: ${error.message}`);
} elseif (error instanceofError) {
// Standard JS Error objects (network failures, etc.)
console.error(`Network error: ${error.message}`);
} else {
// Someone threw a string, number, or something else entirely
console.error('An unexpected error occurred:', String(error));
}
}
}
// Quick demo of the OrderService
const orderService = new OrderService('https://api.example.com', 3);
console.log(orderService.getOrderEndpoint('ORD-4892'));
// Calling fetchOrderDetails (won't actually run without a real server)// fetchOrderDetails('ORD-4892');
Output
https://api.example.com/orders/ORD-4892
// If fetchOrderDetails were called and the API returned a 404:
// API Error 404: Order not found
Interview Gold:
Interviewers love asking about catch clause error typing. The correct answer is: with useUnknownInCatchVariables (part of strict mode since TS 4.4), caught errors are 'unknown' — not 'any'. You must use a type guard or instanceof check before accessing any properties. This forces genuinely safe error handling.
Production Insight
A developer at a travel company wrote a try/catch that assumed every error had a .message property. When a null was thrown (yes, you can throw null), the error handler itself crashed.
useUnknownInCatchVariables forces you to check the type before accessing properties, preventing nested crash in error handlers.
Rule: never assume thrown values are Error objects. Always guard.
Key Takeaway
strictPropertyInitialization prevents undefined class properties at runtime.
useUnknownInCatchVariables forces safe error handling.
Both are small flags with outsized impact on production reliability.
How to handle uninitialized class properties?
IfThe property is assigned in the constructor
→
UseGood — strictPropertyInitialization is satisfied. Just make sure every path assigns it.
IfThe property is assigned after construction (e.g., by a framework)
→
UseUse ! assertion: private foo!: string;. Then ensure the assignment happens before any read.
IfThe property is optional (might never be assigned)
→
UseDeclare it as ? optional: private foo?: string;. It defaults to undefined, which is safe.
strictFunctionTypes and strictBindCallApply: Two Flags Against Subtle Type Holes
These two flags protect against more subtle type errors that often fly under the radar. They're harder to explain than null checks, but they've caught real bugs in real systems.
strictFunctionTypes enforces contravariance on function parameter types. In plain terms: if you have a function that accepts a 'Dog', TypeScript won't let you pass it where a function accepting 'Animal' is expected. Without this flag, you could accidentally do the reverse — assign a more specific function to a broader parameter type — creating a type hole that could slip through.
strictBindCallApply ensures that when you use .bind(), .call(), or .apply(), TypeScript verifies the argument types against the original function signature. Without it, you could pass the wrong number or type of arguments, and the error only appears at runtime.
Both flags are part of the strict bundle and should stay enabled. They prevent category errors that are extremely hard to debug because they only manifest when specific argument types are passed at runtime.
FunctionTypeSafety.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// ================================================================// strictFunctionTypes — Contravariance// ================================================================interfaceAnimal {
name: string;
sound: string;
}
interfaceDogextendsAnimal {
breed: string;
}
// Without strictFunctionTypes, you could assign an 'Dog' parameter handler// to an 'Animal' handler slot — dangerous because the function might call// 'breed' on an Animal that doesn't have it.typeAnimalHandler = (animal: Animal) => void;
typeDogHandler = (dog: Dog) => void;
functionuseHandler(handler: AnimalHandler): void {
handler({ name: 'Generic', sound: '???' });
}
// This assignment is unsafe without strictFunctionTypes:// const dogHandler: DogHandler = (dog: Dog) => console.log(dog.breed);// useHandler(dogHandler); // <-- This would compile without strictFunctionTypes// // but crash at runtime because breed doesn't exist.// WITH strictFunctionTypes enabled, the assignment above is correctly rejected.// ================================================================// strictBindCallApply — Safe invocation methods// ================================================================functionformatGreeting(greeting: string, name: string): string {
return `${greeting}, ${name}!`;
}
// Without strictBindCallApply, these would compile but fail at runtime:// formatGreeting.call(null, 'Hello'); // Missing 'name' argument!// formatGreeting.apply(null, [42, 'World']); // Wrong type for 'greeting'!// With strictBindCallApply enabled:const correct = formatGreeting.call(null, 'Hello', 'World');
// const wrong = formatGreeting.call(null, 'Hello'); // ERROR: Expected 2 arguments, got 1
console.log(correct); // Output: Hello, World!// ================================================================// Real-world example: Event handler binding// ================================================================classButtonUI {
constructor(private label: string) {}
onClick(event: MouseEvent): void {
console.log(`${this.label} clicked at (${event.clientX}, ${event.clientY})`);
}
}
const button = newButtonUI('Submit');
// Without strictBindCallApply, you could accidentally pass the wrong arg:// document.addEventListener('click', button.onClick.bind(button, 'extra-arg'));// This would compile but produce a nonsensical event object.// With strictBindCallApply, it's an error:// document.addEventListener('click', button.onClick.bind(button, 'extra-arg'));// ERROR: Argument of type '[ButtonUI, string]' is not assignable to '[event: MouseEvent]'// Correct usage:
document.addEventListener('click', button.onClick.bind(button));
Output
Hello, World!
(When the button is clicked, console.log prints the label and coordinates)
Pro Tip:
strictFunctionTypes only applies to function types with parameter types that are 'more specific' to the left. It doesn't affect methods — that's a deliberate design choice in TypeScript. If you need contravariance on method parameters, declare them as function properties instead.
Production Insight
A designer tool allowed passing a more specific handler (expecting MouseEvent) to a slot that expected Event. The bug was silent — until a keyboard event triggered the handler and crashed because clientY didn't exist.
strictFunctionTypes would have caught the mismatch at compile time.
Rule: always enable these two flags. They protect against runtime crashes in callback-heavy code.
Both are subtle but critical — they catch category errors that are nearly impossible to debug after they reach production.
When do strictFunctionTypes matter most?
IfYou have callback functions with parameter subtypes
→
UseEnable strictFunctionTypes. It prevents accidental assignments that could lead to runtime type mismatches.
IfYou use .bind(), .call(), or .apply() a lot
→
UseEnable strictBindCallApply. It checks argument types against the function signature, preventing silent argument misalignment.
IfYou see 'Type '...' is not assignable to type '...'' in callback assignments
→
UseThat's strictFunctionTypes working. You likely need to widen the parameter type or accept the error and refactor.
Migrating a Real Project to Strict Mode Without Breaking Everything
The biggest practical question isn't 'what does strict mode do?' — it's 'how do I turn it on in a codebase that didn't start with it?'
The naive approach is to add 'strict: true' and try to fix every error. In a large codebase this can surface hundreds of errors at once, which is demoralising and creates enormous PRs that are hard to review. There's a better strategy.
TypeScript supports per-file override via 'ts-ignore' and the newer '// @ts-nocheck' comment. The migration pattern that actually works in production: enable strict at the project level, immediately suppress all errors in existing files using @ts-nocheck, then remove the suppression comment from each file you touch during normal feature work. The codebase gets safer every sprint without ever blocking progress.
For greenfield projects, there's no excuse — strict: true should be in your tsconfig from the very first commit. The cost of enabling it later grows quadratically with codebase size. CRA, Vite, and Next.js all scaffold with strict mode on by default now, which is the right call.
Two flags worth adding beyond strict: 'noUncheckedIndexedAccess' (array indexing returns T | undefined) and 'exactOptionalPropertyTypes' (truly optional vs. explicitly undefined). Neither is in the strict bundle but both catch real bugs.
MigrationStrategy.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// ================================================================// STEP 1: The problem noUncheckedIndexedAccess solves// (Not in strict bundle — add it separately)// ================================================================const productNames: string[] = ['Laptop', 'Mouse', 'Keyboard'];
// WITHOUT noUncheckedIndexedAccess:// TypeScript types productNames[5] as 'string' — but it's actually undefined at runtime
const firstProduct = productNames[0]; // TypeScript says: string ✓
const missingProduct = productNames[5]; // TypeScript says: string ✗ (it's actually undefined)// console.log(missingProduct.toUpperCase()); // RUNTIME CRASH// WITH noUncheckedIndexedAccess:// TypeScript correctly types productNames[n] as 'string | undefined'// You're forced to check before using itfunctiongetProductName(index: number): string {
const product = productNames[index]; // Type: string | undefinedif (product === undefined) {
return'Unknown Product';
}
return product; // Type narrowed to: string
}
console.log(getProductName(0)); // Laptop
console.log(getProductName(5)); // Unknown Product// ================================================================// STEP 2: Incremental migration pattern// ================================================================// In files not yet migrated, add this at the very top:// // @ts-nocheck// This suppresses ALL TypeScript errors in that file.// It's a tactical retreat, not surrender — remove it file by file.// ================================================================// STEP 3: strictFunctionTypes — a subtle but important flag// ================================================================// This flag enforces contravariance on function parameter types.// Here's a concrete example of the bug it prevents:typeStringHandler = (value: string) => void;
typeAnimalHandler = (animal: { name: string; sound: string }) => void;
// Without strictFunctionTypes, TypeScript allowed assigning a more specific// function type to a broader one — creating a silent type hole.functionprocessWithHandler(
handler: (value: string) => void,
input: string
): void {
handler(input);
}
functionuppercaseHandler(value: string): void {
console.log(`Processed: ${value.toUpperCase()}`);
}
processWithHandler(uppercaseHandler, 'hello world');
// ================================================================// STEP 4: A complete strict-mode tsconfig for a new project// ================================================================
/*
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true, // Functions must always return a value
"noFallthroughCasesInSwitch": true, // switch cases must have break/return"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
}
}
*/
console.log(getProductName(0));
console.log(getProductName(5));
Output
Laptop
Unknown Product
Processed: HELLO WORLD
Laptop
Unknown Product
Pro Tip:
Add 'noImplicitReturns: true' alongside strict mode. It forces every code path in a function to return a value. Without it, a function typed as returning 'string' can silently return 'undefined' if you forget an else branch — and TypeScript won't say a word.
Production Insight
A team migrated a 500-file Next.js app to strict mode in one PR. The PR had 4,000+ changes, took three weeks to review, and introduced new bugs because they had to rush.
The @ts-nocheck incremental strategy would have taken zero weeks of blocked time and zero regression bugs.
Rule: strict mode migration is a marathon, not a sprint. Use @ts-nocheck to isolate new code safety from legacy debt.
Key Takeaway
Don't try to fix all strict mode errors at once. Use @ts-nocheck to suppress existing files.
Add noUncheckedIndexedAccess and exactOptionalPropertyTypes after strict mode.
Greenfield projects: start with strict:true from the first commit — you'll never have to pay the migration cost.
Which flags to add beyond strict?
IfI frequently access arrays with dynamic indices
→
UseAdd noUncheckedIndexedAccess: true. It forces handling of T | undefined at every index access.
IfI want optional properties to truly be optional, not undefined-able
→
UseAdd exactOptionalPropertyTypes: true. It prevents assigning undefined to an optional property without ?.
IfI often miss return statements in complex functions
→
UseAdd noImplicitReturns: true. It ensures every code path returns a value of the declared return type.
noImplicitAny: The Leaky Abstraction That Burns Junior Devs at 2 AM
TypeScript's whole job is to catch type mismatches before they hit production. noImplicitAny is the front door of that promise. Without it, TypeScript silently gives up on variables it can't infer — hammering any into the type and moving on.
The problem isn't that any exists. The problem is that when TypeScript assumesany for you, you don't even know it's happening. A function parameter without a type annotation? That's any. A return value from a third-party lib that isn't typed? That's any. Suddenly your supposedly type-safe codebase is just JavaScript with extra steps.
Why does this matter? Because any disables type checking entirely. You can pass a string where an array is expected, and TypeScript holds your beer. The first time you get undefined is not a function from a .map() call on what you thought was an array, you'll remember this section.
Enable noImplicitAny. Explicitly type every function parameter, every return, every edge case. If you genuinely need any, spell it out — own that decision.
ImplicitAnyTrap.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — javascript tutorial// Without noImplicitAny: TypeScript infers 'any' for 'items' and 'fn'functionprocessItems(items, fn) {
return items.map(fn); // Runtime error if items isn't an array
}
// With noImplicitAny: Compiler screams immediately// Parameter 'items' implicitly has an 'any' type// Parameter 'fn' implicitly has an 'any' type// The fix:function processItems<T>(items: T[], fn: (item: T) => void): void {
items.map(fn); // Now TypeScript validates the contract at compile time
}
// If you really need any, be explicit:functionlegacyHandler(items: any): void {
// I know what I'm doing (or I'm migrating, and this is a stub)
}
Output
Error: Parameter 'items' implicitly has an 'any' type.
Error: Parameter 'fn' implicitly has an 'any' type.
Production Trap:
Third-party JavaScript modules with no type definitions will silently pollute your codebase with any unless you enable noImplicitAny. Always run npm install @types/pkg or use skipLibCheck: true carefully.
Key Takeaway
If TypeScript can't tell what type something is, it should yell at you, not pretend everything is fine.
noImplicitThis: The Silent Killer of Class Context
JavaScript's this is a shape-shifting nightmare. In a class method, this should refer to the class instance — until you pass that method as a callback or strip it off its object. Without noImplicitThis, TypeScript doesn't care. It assumes this is any, and you get runtime this errors that take hours to debug.
noImplicitThis forces you to annotate this when it's ambiguous. This is your early warning system for callback-related context loss. When you see a function that uses this but isn't a method of a class, TypeScript will demand an explicit type annotation like this: YourType.
The payoff is brutal honesty. You either fix the context by using arrow functions or .bind(), or you document that the function expects a specific this context. Either way, the bug surfaces during compilation, not during a customer demo.
Turn it on. It's one flag that saves you from the worst class of JavaScript bugs — the ones that only happen under specific call patterns and are nearly impossible to reproduce.
ImplicitThisBug.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// io.thecodeforge — javascript tutorialclassPaymentHandler {
private amount: number = 100;
process(callback: () => void): void {
callback();
}
logAmount(): void {
console.log(this.amount); // 'this' is PaymentHandler instance
}
}
const handler = newPaymentHandler();
// Without noImplicitThis: This compiles, then breaks at runtime
handler.process(handler.logAmount); // 'this' is undefined in strict mode// Output: TypeError: Cannot read properties of undefined (reading 'amount')// With noImplicitThis: TypeScript catches the implicit 'any' for 'this'// The fix: use an arrow function
handler.process(() => handler.logAmount()); // Works correctly// Or annotate 'this' explicitly in the function signaturefunctionstandaloneFunction(this: { amount: number }): void {
console.log(this.amount);
}
Output
Error: 'this' implicitly has type 'any' because it does not have a type annotation.
When migrating, grep for functions that use this in callbacks. Those are your runtime bombs. Convert them to arrow functions or .bind() before enabling noImplicitThis.
Key Takeaway
Annotate this or lose it. There is no middle ground in strict TypeScript.
Strict Mode Won't Save You From Business Logic Bugs — Here's What Will
Junior devs think strict mode is a magic shield. It's not. It catches type mismatches, null references, and accidental anys. But it won't stop you from passing a valid User object with the wrong role field. That's a business logic bug, and TypeScript stays silent.
Strict mode enforces structural correctness — your types match. It does not enforce semantic correctness — your data makes sense. The gap between a string and a valid email address is infinite. No amount of strict: true closes that.
Production code needs both: strict mode for the compiler, runtime validation for the real world. Use Zod or io-ts at the boundaries. Validate API responses. Validate user input. Treat strict mode as the floor, not the ceiling. Your 2 AM pager will thank you.
Strict mode + no runtime validation = false sense of security. Every API boundary must validate. TypeScript is compile-time only — your server runs in a dynamic world.
Key Takeaway
Strict mode catches type bugs. Runtime validation catches real-world data bugs. You need both in production.
Enabling Strict Mode Mid-Project Is a Betrayal — But Here's How to Survive
You inherit a codebase with 50,000 lines of JavaScript pretending to be TypeScript. You flip strict: true. Suddenly, 1,400 red squiggly lines appear. Your team panics. The correct response is not to revert — it's to isolate the pain.
Add // @ts-nocheck at the file level for the worst offenders. Then work through files one by one. Start with the files that touch external APIs — these are your highest risk. Replace any with unknown first, then narrow. Fix null checks in data transformations.
CI pipeline? Add an eslint rule that bans // @ts-ignore but allows // @ts-expect-error with a reason. Track strict-mode coverage as a build metric. Every week, your threshold increases. This is how you migrate a battleship without sinking it.
migrateStrict.tsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// io.thecodeforge — javascript tutorial// Step 1: Isolate worst files with @ts-nocheck// Step 2: Fix one at a time, then remove the comment// Before: any hellfunctionfetchUser(id: any): any {
returnfetch(`/users/${id}`).then(r => r.json());
}
// After: strict + unknown
interface ApiResponse {
id: string;
name: string | null;
email: string;
}
asyncfunctionfetchUserStrict(id: string): Promise<ApiResponse> {
const raw: unknown = awaitfetch(`/users/${id}`).then(r => r.json());
// Runtime check — no cheatingif (
typeof raw !== 'object' ||
raw === null ||
!('id'in raw) ||
!('email'in raw)
) {
thrownewError('Invalid API response shape');
}
return raw as ApiResponse;
}
Output
No output — compilation succeeds with strict: true
Senior Shortcut:
Start migration at the data layer — models, API clients, database queries. Fixing those cascades correct types into every consumer. Surface-level UI files can wait.
Key Takeaway
Migrate to strict mode incrementally: isolate with @ts-nocheck, fix high-risk files first, enforce coverage thresholds in CI.
● Production incidentPOST-MORTEMseverity: high
The Null-Pointer Crash That Strict Mode Would Have Caught
Symptom
Users reported 'Cannot read properties of null (reading 'email')' on the checkout confirmation page. The error occurred only for accounts created via a legacy API that didn't return a full user object.
Assumption
The team assumed that every user returned from the database always had an email field populated. The function signature was getUser(id: number): User without any nullable indication.
Root cause
strictNullChecks was disabled. The function actually returned User | null for legacy users, but TypeScript allowed the caller to treat it as always User. The null slipped through because no one checked the return value.
Fix
Enabled strictNullChecks. Changed the return type to User | null and updated all callers to handle the null case with early returns or fallback logic. Added a unit test that reproduces the legacy user path.
Key lesson
Never trust implicit null handling. Always enable strictNullChecks — it catches nulls at compile time that otherwise crash at runtime.
When migrating a legacy codebase, fix the null-safety issues first. They're the most common source of silent production failures.
Run tsc --showConfig in CI to ensure the build matches local compiler settings.
Production debug guideSymptom → Action guide for the most common strict mode issues in CI and local development.5 entries
Symptom · 01
Your IDE shows no errors, but tsc in CI fails with strict mode errors.
→
Fix
Run tsc --showConfig to verify the resolved tsconfig. CI often uses a different tsconfig.build.json or overrides strict: false. Align all configs.
Symptom · 02
You get 'Object is possibly 'undefined'' on array[index].
→
Fix
Add noUncheckedIndexedAccess: true to your config. It forces you to handle T | undefined when indexing into arrays and objects.
Symptom · 03
Caught errors in catch blocks complain about accessing .message.
→
Fix
With useUnknownInCatchVariables (part of strict), the error is unknown. Use error instanceof Error to narrow, or write a custom type guard.
Symptom · 04
Class properties are flagged as 'not definitely assigned'.
→
Fix
Assign the property inline, in the constructor, or use the definite assignment assertion ! if it's set after construction (e.g., by a DI framework).
Symptom · 05
Function parameter types cause 'Type 'X' is not assignable to type 'Y'' when using .bind().
→
Fix
Enable strictBindCallApply. It catches mismatches between the function signature and the arguments passed to .bind(), .call(), .apply().
★ Quick Debug Cheat Sheet: Strict Mode ErrorsFive minute fixes for the most annoying strict mode errors you'll hit in production.
Object is possibly 'undefined' at `user.email`−
Immediate action
Check if `user` could be null. Use optional chaining: `user?.email` or a guard: `if (user) { user.email }`
Commands
tsc --noEmit --strictNullChecks
tsc --showConfig | grep strictNullChecks
Fix now
Add if (user) { around the usage, or set a default: const email = user?.email ?? 'unknown'
Element implicitly has an 'any' type+
Immediate action
Find where the parameter or variable is declared without a type annotation.
Commands
tsc --noEmit --noImplicitAny
grep -rn 'function.*(.*)' src/ | head -20
Fix now
Add an explicit type annotation: function process(data: string): void { ... }
Cannot invoke an object which is possibly 'undefined'+
Immediate action
Check if the function reference might be undefined. Use optional call syntax: `callback?.()`.
Commands
tsc --noEmit --strictNullChecks
cat src/features/checkout.ts | grep -n 'callback'
Fix now
Replace callback() with callback?.() and handle the case when it's undefined.
Property 'message' does not exist on type 'unknown'+
Immediate action
Use `error instanceof Error` to narrow the type before accessing `.message`.
Untyped function params/variables silently typed as 'any'
Type safety silently disabled for entire code paths
strictPropertyInitialization
Class properties declared but never assigned
Accessing a property that's secretly undefined at runtime
useUnknownInCatchVariables
Accessing .message on a caught error without type checking
Crashing in your error handler — the worst place to crash
strictFunctionTypes
Unsafe function type assignments that create type holes
Functions receiving the wrong argument types with no error
strictBindCallApply
.call()/.bind()/.apply() called with wrong argument types
Type-unsafe invocations that only fail at runtime
noUncheckedIndexedAccess (extra)
Array/object indexing that might return undefined
Treating a potentially missing value as definitely present
exactOptionalPropertyTypes (extra)
Optional properties that should not be explicitly set to undefined
Accidentally setting undefined where the API expected the property to be absent
Key takeaways
1
'strict
true' is a shorthand for eight separate flags — knowing each one individually means you can diagnose errors instantly and enable them incrementally during migration.
2
strictNullChecks and noImplicitAny are the two highest-impact flags
between them they prevent the two most common categories of TypeScript bugs: null dereferences and silent any leakage.
3
useUnknownInCatchVariables types caught errors as 'unknown', not 'any'
this forces you to write real error handling instead of assuming every thrown value is a standard Error object.
4
noUncheckedIndexedAccess and exactOptionalPropertyTypes aren't in the strict bundle but are worth adding to every greenfield project
they close real gaps that strict alone leaves open.
5
The @ts-nocheck migration strategy lets you adopt strict mode incrementally without blocking development
enable strict globally, suppress old files, fix them one by one during normal work.
Common mistakes to avoid
4 patterns
×
Using '!' (non-null assertion) everywhere to silence strictNullChecks errors
Symptom
Code compiles but still crashes with TypeError in production. The '!' operator tells TypeScript 'this is definitely not null' — if you're wrong, TypeScript can't help you.
Fix
Narrow with an actual null check (if statement or optional chaining) instead of asserting your way out of the error.
×
Setting strict: true in tsconfig but having a tsconfig.app.json or tsconfig.build.json that overrides it with strict: false
Symptom
Errors appear in your IDE but not in CI, or vice versa. Leads to false confidence and shipping bugs.
Fix
Run 'tsc --showConfig' (not 'tsc --version') to see the actual resolved config your build is using. Always verify the file your build tool is actually reading.
×
Typing caught errors as 'any' manually to work around useUnknownInCatchVariables
Symptom
Developers write 'catch (error: any)' to get the old behaviour back. This defeats the entire purpose of the flag.
Fix
Write a type guard function (like 'isApiError' shown above) or use 'instanceof Error' for standard errors. The two-minute effort to write a guard prevents hours of debugging a broken error handler.
×
Ignoring noUncheckedIndexedAccess because 'my arrays are always complete'
Symptom
Accessing array elements with dynamic indices returns undefined at runtime, crashing your UI or service.
Fix
Enable noUncheckedIndexedAccess. Then use early returns, optional chaining, or Array methods like .at() that return undefined. Your code will be safer and more explicit.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
What's the difference between enabling 'strict: true' and manually listi...
Q02SENIOR
With strictNullChecks enabled, how would you safely handle a function th...
Q03SENIOR
Why are caught errors typed as 'unknown' rather than 'Error' in strict m...
Q04SENIOR
What is the definite assignment assertion `!` and when is it appropriate...
Q01 of 04SENIOR
What's the difference between enabling 'strict: true' and manually listing all the strict flags individually in tsconfig? When would you choose one over the other?
ANSWER
strict: true is shorthand. It enables all eight flags at once. Manually listing them allows you to exclude specific flags that might be too disruptive during a migration. For example, you could enable noImplicitAny and strictNullChecks but leave strictFunctionTypes off until you've fixed callback-heavy areas. However, for greenfield projects, always use strict: true — it's simpler and ensures you get all protections.
Q02 of 04SENIOR
With strictNullChecks enabled, how would you safely handle a function that might return null or undefined? Walk me through at least two patterns.
ANSWER
Two patterns: 1) Early return with a guard: const result = getData(); if (result === null) return default;. 2) Optional chaining with fallback: const name = user?.name ?? 'Unknown';. The key is that TypeScript narrows the type within the conditional block. You can also use the nullish coalescing operator ?? to provide a default when the value is null or undefined.
Q03 of 04SENIOR
Why are caught errors typed as 'unknown' rather than 'Error' in strict mode, and what does that tell you about how TypeScript thinks about exception safety?
ANSWER
In JavaScript, you can throw anything — strings, numbers, objects, null, even undefined. If TypeScript typed caught errors as Error, you'd have a false sense of safety because the actual thrown value might not have a .message property. By typing them as unknown, TypeScript forces you to write code that actually checks what was thrown before using it. This reflects TypeScript's design philosophy: don't assume — verify. It's the same principle that makes strictNullChecks so powerful: honest types prevent silent assumptions.
Q04 of 04SENIOR
What is the definite assignment assertion `!` and when is it appropriate to use it?
ANSWER
The ! operator tells TypeScript 'this value is definitely not null/undefined at this point' or 'this property will be assigned before it's read'. Appropriate uses: 1) Class properties set by a dependency injection framework after instantiation. 2) Variables that are assigned inside a callback that runs synchronously (e.g., let result!: string; setTimeout(() => result = 'done', 0); — but be careful with async). 3) When you have a guard upstream that TypeScript can't reason about. Inappropriate uses: as a lazy workaround for real nullable logic. Overusing ! erodes the safety strict mode provides.
01
What's the difference between enabling 'strict: true' and manually listing all the strict flags individually in tsconfig? When would you choose one over the other?
SENIOR
02
With strictNullChecks enabled, how would you safely handle a function that might return null or undefined? Walk me through at least two patterns.
SENIOR
03
Why are caught errors typed as 'unknown' rather than 'Error' in strict mode, and what does that tell you about how TypeScript thinks about exception safety?
SENIOR
04
What is the definite assignment assertion `!` and when is it appropriate to use it?
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
Does enabling strict mode in TypeScript affect runtime performance?
No. Strict mode is entirely a compile-time feature. It only changes what the TypeScript compiler checks and rejects — the JavaScript it emits is identical whether strict is on or off. All the type information is erased during compilation.
Was this helpful?
02
Should I enable strict mode in an existing JavaScript project I'm migrating to TypeScript?
Yes — but do it incrementally. Enable 'strict: true' immediately, then add '// @ts-nocheck' to every file that breaks. Remove the suppression comment file by file as you work through the codebase. This gives you the safety net on new code right away without blocking the migration.
Was this helpful?
03
What's the difference between 'unknown' and 'any' in catch clauses?
With 'any', TypeScript lets you access any property without checks — it trusts you completely. With 'unknown', TypeScript refuses to let you access any property until you prove the type via a type guard or instanceof check. 'unknown' is the safe default: it forces you to handle the possibility that the thrown value isn't what you expect.
Was this helpful?
04
Can I mix strict and non-strict files in the same project?
Technically no — strict is a project-wide compiler option. However, you can simulate per-file strictness by adding '// @ts-nocheck' at the top of files you haven't fixed yet. Those files are effectively ignored, and new code in other files benefits from full strict checking. This is the recommended migration approach.
Was this helpful?
05
Is there any reason NOT to enable strict mode?
Only during an incremental migration of a very large, untyped codebase where fixing all issues at once would block development for weeks. Even then, you should still enable strict at the project level and suppress old files. For new projects, there is no valid reason to disable it.