TypeScript Strict Mode — Enable It, Fix the Errors, Ship Safer Code
Advertisement
Introduction
Why This Matters
Strict mode is TypeScript's highest-leverage setting. A single line in tsconfig.json activates eight safety flags that collectively catch the most common categories of runtime bugs: null dereference, implicit any, missing return paths, function type mismatches, and uninitialized properties. Teams that enable strict mode from the start report dramatically fewer production incidents.
For backend engineers, the value is compounded. APIs deal with user-supplied data, database records, and third-party responses — all sources of unexpected null values and shape mismatches. Strict mode forces you to handle every edge case explicitly at the point where data enters your system, rather than discovering it as a 500 error.
The initial migration cost for an existing project is real — you will see errors. But every error strict mode surfaces is a latent bug. Fix them at compile time, not in your incident post-mortem.
Enabling Strict Mode
{
"compilerOptions": {
"strict": true
}
}strict: true is a shorthand that activates all of the following flags simultaneously.
noImplicitAny
Prevents variables and parameters from silently defaulting to any when no type can be inferred.
// Without strict — silently any
function multiply(a, b) {
return a * b; // a and b are any — no protection
}
// With strict — must be explicit
function multiply(a: number, b: number): number {
return a * b;
}
// Common migration pattern for third-party callbacks
someArray.forEach((item: unknown) => {
// Now you must narrow the type before using it
if (typeof item === 'string') {
console.log(item.toUpperCase());
}
});strictNullChecks
The most impactful flag. Without it, null and undefined are assignable to every type — enabling the billion-dollar null reference error.
// strictNullChecks: false — this compiles and crashes at runtime
function getUser(id: string): User {
return db.find(id); // could be null — no warning
}
getUser('missing').email; // TypeError at runtime
// strictNullChecks: true — must handle the null case
function getUser(id: string): User | null {
return db.find(id);
}
const user = getUser('missing');
if (!user) throw new Error('User not found');
user.email; // TypeScript knows user is not null here
// Useful patterns for null handling
const name = user?.profile?.displayName ?? 'Anonymous';
const port = parseInt(process.env.PORT ?? '3000', 10);noImplicitThis
Prevents this from having an implicit any type inside functions.
// Error — TypeScript can't infer the type of 'this'
function getDisplayName() {
return this.firstName + ' ' + this.lastName; // 'this' implicitly has type 'any'
}
// Fixed — declare 'this' as the first fake parameter
function getDisplayName(this: { firstName: string; lastName: string }): string {
return `${this.firstName} ${this.lastName}`;
}
// Arrow functions in classes avoid the issue entirely
class UserModel {
firstName = 'Alice';
lastName = 'Smith';
getDisplayName = (): string => {
return `${this.firstName} ${this.lastName}`; // 'this' is always the class instance
};
}strictFunctionTypes
Enforces correct variance for function parameter types. Parameters must be contravariant.
type StringProcessor = (value: string) => void;
type StringOrNumberProcessor = (value: string | number) => void;
// Error — a function expecting only string cannot be assigned to one that accepts string | number
const processor: StringOrNumberProcessor = (value: string) => console.log(value.toUpperCase());
// Correct — the assigned function must handle all types the variable type allows
const processor: StringOrNumberProcessor = (value: string | number) => console.log(String(value));strictPropertyInitialization
All class properties must be initialized in the constructor or declared with a definite assignment assertion.
// Error — properties are declared but never assigned
class UserService {
db: Database; // Error: not initialized
logger: Logger; // Error: not initialized
}
// Fixed — initialize in constructor
class UserService {
private readonly db: Database;
private readonly logger: Logger;
constructor(db: Database, logger: Logger) {
this.db = db;
this.logger = logger;
}
}
// For lazy initialization — use definite assignment assertion sparingly
class ConfigService {
private config!: Record<string, string>; // ! means "I guarantee this is set before use"
async load(): Promise<void> {
this.config = await loadConfigFromFile();
}
}strictBindCallApply
Ensures bind, call, and apply receive correctly typed arguments.
function formatCurrency(amount: number, currency: string): string {
return `${currency}${amount.toFixed(2)}`;
}
// Error — missing the 'currency' argument
const bound = formatCurrency.bind(null, 100);
bound(); // Error: Expected 1 arguments, but got 0
// Correct
const bound = formatCurrency.bind(null, 100);
bound('$'); // OK — provides the missing 'currency' argumentnoImplicitReturns and noFallthroughCasesInSwitch
// noImplicitReturns — all code paths must return
function classify(score: number): 'pass' | 'fail' | 'distinction' {
if (score >= 90) return 'distinction';
if (score >= 60) return 'pass';
// Error: Not all code paths return a value
}
// Fixed
function classify(score: number): 'pass' | 'fail' | 'distinction' {
if (score >= 90) return 'distinction';
if (score >= 60) return 'pass';
return 'fail';
}
// noFallthroughCasesInSwitch
function handleStatus(status: string): void {
switch (status) {
case 'active':
console.log('User is active');
// Error: Fallthrough case in switch
case 'pending':
console.log('Needs review');
break;
}
}Incremental Migration Strategy
For existing codebases, enable flags one at a time rather than all at once.
{
"compilerOptions": {
"noImplicitAny": true
}
}Recommended order:
noImplicitAny— biggest return on investmentstrictNullChecks— catches null dereference bugsstrictPropertyInitialization— enforces DI patternsstrictFunctionTypes— usually few errors- Enable
strict: trueto catch remaining flags
// Use 'unknown' during migration for untyped external data
function processWebhook(payload: unknown): void {
// Narrow the type before use
if (
typeof payload === 'object' &&
payload !== null &&
'event' in payload &&
typeof (payload as { event: unknown }).event === 'string'
) {
console.log((payload as { event: string }).event);
}
}Common Mistakes
- Using
!(non-null assertion) too liberally to silence strict errors instead of fixing them - Enabling
strict: truewithout reviewing every error — some indicate real bugs - Using
anyas a migration shortcut and never coming back to fix it - Disabling
strictNullChecksbecause it surfaces many errors — those are real bugs - Forgetting to update
.d.tsdeclaration files when fixing strict mode errors in library code
Best Practices
- Always start new projects with
strict: true— the cost is near zero at project inception - Use
eslint-plugin-@typescript-eslintwithno-explicit-anyto enforce noanyin code review - Treat
// @ts-ignoreand// @ts-expect-erroras technical debt that needs a linked issue - Write type guards for all
unknowndata from external sources (HTTP requests, database rows, env vars) - Use exhaustive checking with
neverfor switch statements over union types
Key Takeaways
strict: trueis a single tsconfig flag that enables eight compile-time safety checksstrictNullChecksis the most impactful flag — it eliminates entire classes of null dereference errorsnoImplicitAnyforces explicit type annotations, making function contracts obvious and auditablestrictPropertyInitializationrequires constructor injection patterns, improving testabilitynoImplicitReturnscatches missing return paths in functions with complex control flow- All strict mode flags are compile-time only — they have zero runtime performance impact
- For existing projects, migrate flag by flag starting with
noImplicitAnyandstrictNullChecks - Every error strict mode surfaces is a latent bug — treat them as value, not friction
Advertisement