Difficulty: Advanced
TypeScript's strict mode is a meta-flag that enables a group of stricter type-checking options. When you set `strict: true` in tsconfig.json, it activates strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, and alwaysStrict. Each flag addresses a specific category of type safety issues, and understanding them individually is essential for interviews and debugging.
`strictNullChecks` is arguably the most impactful flag. Without it, null and undefined are assignable to every type - a string variable could be null, and TypeScript would not warn you. With strictNullChecks enabled, null and undefined are only assignable to their own types and void. This forces you to handle null cases explicitly, preventing the most common runtime error in JavaScript: "Cannot read property of null/undefined".
`noImplicitAny` prevents TypeScript from silently inferring the `any` type when it cannot determine a more specific type. Without this flag, function parameters without type annotations get the `any` type, effectively disabling type checking for those values. With noImplicitAny, you must provide explicit type annotations where inference is not possible, ensuring complete type coverage.
`strictFunctionTypes` enables contravariant checking of function parameter types. Without it, function parameters are checked bivariantly - both covariantly and contravariantly - which is unsound. With strictFunctionTypes, assigning a function that accepts a base type to a variable typed to accept a derived type is correctly flagged as an error. This is particularly important when working with callback hierarchies and event handlers.
// With strictNullChecks: true
// null and undefined are NOT assignable to other types
function findUser(id: number): { name: string } | null {
if (id === 1) return { name: 'Alice' };
return null;
}
const user = findUser(1);
// Must check for null before accessing properties
if (user !== null) {
console.log(`Found: ${user.name}`);
} else {
console.log('User not found');
}
// Using optional chaining + nullish coalescing
const user2 = findUser(2);
console.log(`Name: ${user2?.name ?? 'Unknown'}`);
// Non-null assertion (use sparingly)
const user3 = findUser(1);
console.log(`Asserted: ${user3!.name}`);
strictNullChecks forces you to handle null explicitly. Use narrowing (if checks), optional chaining (?.), nullish coalescing (??), or non-null assertion (!) to access values that may be null.
// With noImplicitAny: true
// Parameters without annotations cause errors if type cannot be inferred
// ERROR without annotation: Parameter 'x' implicitly has an 'any' type
// function double(x) { return x * 2; }
// CORRECT: explicit annotation
function double(x: number): number {
return x * 2;
}
// Inference works for variables - no annotation needed
const result = double(5); // TypeScript infers: number
console.log(`Double: ${result}`);
// Array methods infer callback types from the array
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2); // n is inferred as number
console.log(`Doubled: ${doubled.join(', ')}`);
// Object destructuring also infers
const config = { host: 'localhost', port: 3000 };
const { host, port } = config; // both correctly inferred
console.log(`Server: ${host}:${port}`);
noImplicitAny requires explicit types where TypeScript cannot infer them - mainly function parameters. Variables, return values, and callback parameters are usually inferred.
// strictFunctionTypes enables contravariant parameter checking
class Animal {
name: string;
constructor(name: string) { this.name = name; }
}
class Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
}
// Function type hierarchy
type AnimalHandler = (animal: Animal) => void;
type DogHandler = (dog: Dog) => void;
// Safe: AnimalHandler can be used where DogHandler is expected?
// NO - with strictFunctionTypes, this would be an error:
// const handler: DogHandler = (animal: Animal) => { ... }; // OK - accepting wider type
// const handler: AnimalHandler = (dog: Dog) => { console.log(dog.breed); }; // ERROR - unsafe
// Demonstration of why this matters
const animalHandler: AnimalHandler = (animal: Animal) => {
console.log(`Animal: ${animal.name}`);
};
// This is safe - handler accepts any Animal, Dog is an Animal
const dog = new Dog('Rex', 'Shepherd');
animalHandler(dog);
// The reverse is unsafe because:
const dogHandler: DogHandler = (dog: Dog) => {
console.log(`Dog: ${dog.name}, Breed: ${dog.breed}`);
};
dogHandler(new Dog('Buddy', 'Labrador'));
console.log('strictFunctionTypes prevents unsound assignments');
strictFunctionTypes ensures that when assigning functions, parameter types are checked contravariantly. A function expecting a Dog parameter cannot be assigned where an Animal handler is expected, because it might access Dog-specific properties on a plain Animal.
strict, strictNullChecks, noImplicitAny, strictFunctionTypes, strictPropertyInitialization, noImplicitReturns, noUncheckedIndexedAccess