Back to Blog
TypeScriptStrict ModeType SafetyConfiguration

TypeScript Strict Mode Explained

Enable and understand TypeScript strict mode. From compiler flags to common errors to migration strategies.

B
Bootspring Team
Engineering
May 28, 2021
6 min read

Strict mode catches more bugs at compile time. Here's how to use it effectively.

Enabling Strict Mode#

1// tsconfig.json 2{ 3 "compilerOptions": { 4 "strict": true 5 // Or enable individually: 6 // "strictNullChecks": true, 7 // "strictFunctionTypes": true, 8 // "strictBindCallApply": true, 9 // "strictPropertyInitialization": true, 10 // "noImplicitAny": true, 11 // "noImplicitThis": true, 12 // "alwaysStrict": true, 13 // "useUnknownInCatchVariables": true 14 } 15}

strictNullChecks#

1// Without strictNullChecks 2function getLength(str: string) { 3 return str.length; // Works even if str is null/undefined at runtime 4} 5 6// With strictNullChecks 7function getLength(str: string) { 8 return str.length; // str cannot be null/undefined 9} 10 11function getLengthSafe(str: string | null) { 12 if (str === null) { 13 return 0; 14 } 15 return str.length; // str is narrowed to string 16} 17 18// Optional properties 19interface User { 20 name: string; 21 email?: string; // string | undefined 22} 23 24function sendEmail(user: User) { 25 // Error: Object is possibly 'undefined' 26 // console.log(user.email.toLowerCase()); 27 28 // Fix: Check for undefined 29 if (user.email) { 30 console.log(user.email.toLowerCase()); 31 } 32 33 // Or use optional chaining 34 console.log(user.email?.toLowerCase()); 35 36 // Or nullish coalescing 37 console.log(user.email ?? 'no email'); 38}

noImplicitAny#

1// Without noImplicitAny 2function process(data) { // data is implicitly 'any' 3 return data.value; 4} 5 6// With noImplicitAny - Error: Parameter 'data' implicitly has an 'any' type 7function process(data) { 8 return data.value; 9} 10 11// Fix: Add explicit types 12function process(data: { value: string }) { 13 return data.value; 14} 15 16// Or if any is intended 17function process(data: any) { 18 return data.value; 19} 20 21// Array methods 22const items = [1, 2, 3]; 23 24// Error without explicit type 25items.map(item => item * 2); // item implicitly 'any' in some contexts 26 27// Fix: explicit or inferred 28items.map((item: number) => item * 2);

strictFunctionTypes#

1// Function parameter contravariance 2 3interface Animal { 4 name: string; 5} 6 7interface Dog extends Animal { 8 breed: string; 9} 10 11// Without strictFunctionTypes 12let animalHandler: (animal: Animal) => void; 13let dogHandler: (dog: Dog) => void = (dog) => console.log(dog.breed); 14 15animalHandler = dogHandler; // Allowed (but unsafe!) 16 17// With strictFunctionTypes - Error 18animalHandler = dogHandler; 19// Type '(dog: Dog) => void' is not assignable to type '(animal: Animal) => void' 20 21// Safe assignment (contravariance) 22dogHandler = animalHandler; // OK - animal handlers can handle dogs 23 24// Method vs function syntax 25interface Handlers { 26 // Method syntax (bivariant - less strict) 27 method(animal: Animal): void; 28 29 // Function syntax (contravariant - strict) 30 func: (animal: Animal) => void; 31}

strictBindCallApply#

1// Checks bind, call, apply arguments 2 3function greet(name: string, age: number) { 4 return `Hello ${name}, you are ${age}`; 5} 6 7// Without strictBindCallApply 8greet.call(undefined, 'John', 'thirty'); // No error (string instead of number) 9 10// With strictBindCallApply - Error 11greet.call(undefined, 'John', 'thirty'); 12// Argument of type 'string' is not assignable to parameter of type 'number' 13 14// Correct usage 15greet.call(undefined, 'John', 30); 16greet.apply(undefined, ['John', 30]); 17greet.bind(undefined, 'John')(30);

strictPropertyInitialization#

1// Class properties must be initialized 2 3// Error: Property 'name' has no initializer 4class User { 5 name: string; 6 7 constructor() { 8 // name is not initialized 9 } 10} 11 12// Fix 1: Initialize in constructor 13class User { 14 name: string; 15 16 constructor(name: string) { 17 this.name = name; 18 } 19} 20 21// Fix 2: Default value 22class User { 23 name: string = ''; 24} 25 26// Fix 3: Definite assignment assertion 27class User { 28 name!: string; // Tell TS it will be initialized 29 30 init(name: string) { 31 this.name = name; 32 } 33} 34 35// Fix 4: Optional property 36class User { 37 name?: string; 38}

noImplicitThis#

1// this must have explicit type 2 3// Error: 'this' implicitly has type 'any' 4function getName() { 5 return this.name; 6} 7 8// Fix: Explicit this parameter 9function getName(this: { name: string }) { 10 return this.name; 11} 12 13// Class methods are fine (this is inferred) 14class User { 15 name = 'John'; 16 17 getName() { 18 return this.name; // this is User 19 } 20} 21 22// Arrow functions don't have their own this 23const user = { 24 name: 'John', 25 getName: function() { 26 return this.name; // OK, this is typed 27 }, 28 getNameArrow: () => { 29 // return this.name; // Error: 'this' is not from user 30 }, 31};

useUnknownInCatchVariables#

1// catch variables are unknown, not any 2 3try { 4 throw new Error('Something went wrong'); 5} catch (error) { 6 // Without: error is 'any' 7 // With: error is 'unknown' 8 9 // Error: Object is of type 'unknown' 10 // console.log(error.message); 11 12 // Fix: Type guard 13 if (error instanceof Error) { 14 console.log(error.message); 15 } 16 17 // Or type assertion 18 console.log((error as Error).message); 19} 20 21// Custom error handling 22function isError(error: unknown): error is Error { 23 return error instanceof Error; 24} 25 26try { 27 riskyOperation(); 28} catch (error) { 29 if (isError(error)) { 30 console.log(error.message); 31 } else { 32 console.log('Unknown error:', error); 33 } 34}

Migrating to Strict Mode#

1// Gradual migration in tsconfig.json 2{ 3 "compilerOptions": { 4 // Start with these 5 "noImplicitAny": true, 6 "strictNullChecks": true, 7 8 // Add later 9 "strictFunctionTypes": true, 10 "strictBindCallApply": true, 11 "strictPropertyInitialization": true, 12 "noImplicitThis": true, 13 "useUnknownInCatchVariables": true, 14 15 // Finally 16 "strict": true 17 } 18}
1// Common patterns for fixing strict errors 2 3// Null checks 4function process(value: string | null) { 5 // Using if 6 if (value !== null) { 7 return value.toUpperCase(); 8 } 9 10 // Using optional chaining 11 return value?.toUpperCase(); 12 13 // Using nullish coalescing 14 return (value ?? '').toUpperCase(); 15 16 // Using assertion (when you know better) 17 return value!.toUpperCase(); 18} 19 20// Type assertions for external data 21const data = JSON.parse(jsonString) as MyType; 22 23// Or with validation 24function isMyType(data: unknown): data is MyType { 25 return ( 26 typeof data === 'object' && 27 data !== null && 28 'requiredField' in data 29 ); 30} 31 32const data = JSON.parse(jsonString); 33if (isMyType(data)) { 34 // data is MyType 35}

Best Practices#

Migration: ✓ Enable flags incrementally ✓ Fix one file at a time ✓ Use // @ts-expect-error for known issues ✓ Add tests before migrating Patterns: ✓ Use type guards for narrowing ✓ Prefer unknown over any ✓ Initialize class properties ✓ Handle null/undefined explicitly Avoid: ✗ Using ! assertion excessively ✗ Casting to any to silence errors ✗ Disabling rules per-file ✗ Ignoring compiler warnings

Conclusion#

Strict mode catches bugs that would otherwise appear at runtime. Enable it for new projects and migrate existing ones gradually. The initial investment in fixing errors pays off with more reliable code and better developer experience through improved editor support.

Share this article

Help spread the word about Bootspring