Difficulty: Intermediate
What are type guards? How does TypeScript narrow types based on runtime checks?
Type guards are runtime checks that TypeScript uses to narrow a type within a conditional block. When you check a condition, TypeScript automatically narrows the type in that branch.
Built-in narrowing: - typeof: for primitives (string, number, boolean) - instanceof: for class instances - in: for checking property existence - Equality checks: === null, !== undefined
Custom type guards: functions that return 'value is Type'
function process(value: string | number | null) {
// typeof narrowing
if (typeof value === 'string') {
console.log(value.toUpperCase()); // OK: string
} else if (typeof value === 'number') {
console.log(value.toFixed(2)); // OK: number
} else {
console.log(value); // OK: null
}
}
// instanceof narrowing
function handleError(err: Error | string) {
if (err instanceof Error) {
console.log(err.message); // OK: Error
console.log(err.stack); // OK: Error
} else {
console.log(err.toUpperCase()); // OK: string
}
}
// 'in' operator
type Dog = { bark(): void };
type Cat = { meow(): void };
function speak(pet: Dog | Cat) {
if ('bark' in pet) {
pet.bark(); // OK: Dog
} else {
pet.meow(); // OK: Cat
}
}
TypeScript's control flow analysis automatically narrows types based on these checks. Each branch has a more specific type.
interface Fish { swim(): void; name: string }
interface Bird { fly(): void; name: string }
// Custom type guard with 'is' return type
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // OK: narrowed to Fish
} else {
pet.fly(); // OK: narrowed to Bird
}
}
// Assertion function (throws if not valid)
function assertIsString(val: unknown): asserts val is string {
if (typeof val !== 'string') {
throw new Error(`Expected string, got ${typeof val}`);
}
}
function greet(name: unknown) {
assertIsString(name);
console.log(name.toUpperCase()); // OK: narrowed to string
}
Custom type guards return 'value is Type'. Assertion functions use 'asserts value is Type' and throw if the assertion fails.
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'triangle'; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius 2;
case 'square':
return shape.side 2;
case 'triangle':
return (shape.base * shape.height) / 2;
default:
// Exhaustive check - if we add a new shape
// and forget to handle it, this line errors
const _exhaustive: never = shape;
return _exhaustive;
}
}
Assigning to 'never' in the default case ensures all union members are handled. If you add a new shape variant, TypeScript will error here until you add a case for it.
Type Guards, Narrowing, typeof, instanceof, in operator, Custom Guards