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.