On this page
Type Guards and Narrowing
TypeScript’s static analysis understands when a value’s type becomes more specific. Narrowing is the process of refining a broad type to a narrower one based on runtime checks.
The Problem
function printId(id: string | number) {
// console.log(id.toUpperCase()); // Error: toUpperCase doesn't exist on number
console.log(id);
}
We need to tell TypeScript which branch we are in.
typeof Guards
function printId(id: string | number): void {
if (typeof id === 'string') {
console.log(id.toUpperCase()); // id is string here
} else {
console.log(id.toFixed(2)); // id is number here
}
}
printId('abc-123');
printId(42);
typeof works for primitives: string, number, boolean, bigint, symbol, undefined, function.
instanceof Guards
Check against class constructors:
class Dog {
bark() { return 'Woof!'; }
}
class Cat {
meow() { return 'Meow!'; }
}
function speak(animal: Dog | Cat): string {
if (animal instanceof Dog) {
return animal.bark();
}
return animal.meow();
}
in Operator
Check for property existence:
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function move(animal: Bird | Fish): void {
if ('fly' in animal) {
animal.fly();
} else {
animal.swim();
}
}
Discriminated Unions
Add a shared literal property (discriminant) to each variant:
interface Circle {
kind: 'circle';
radius: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
type Shape = Circle | Rectangle;
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
}
}
TypeScript knows the exact shape in each case branch.
Custom Type Guards
Return a type predicate with is:
interface Admin {
role: 'admin';
permissions: string[];
}
interface RegularUser {
role: 'user';
name: string;
}
type Account = Admin | RegularUser;
function isAdmin(account: Account): account is Admin {
return account.role === 'admin';
}
function handleAccount(account: Account) {
if (isAdmin(account)) {
console.log(account.permissions);
} else {
console.log(account.name);
}
}
Narrowing is essential for working with unions and external data. Next, we explore advanced type composition techniques.