Type Narrowing
Narrowing is how TypeScript figures out, inside a specific branch of your code, that a value is actually one particular member of a union rather than any of the others — usually because of a check you already wrote for a completely ordinary reason.
typeof narrowing
function printId(id: number | string) { if (typeof id === "string") { console.log(id.toUpperCase()); } else { console.log(id.toFixed(2)); } } printId("a9x"); printId(3.14159);
A9X 3.14
Inside the if branch, TypeScript knows id can only be a string — the typeof check itself is what narrows the union down, so .toUpperCase() is allowed there without any extra casting. In the else branch, it's narrowed to number instead, so .toFixed() is allowed.
instanceof and property checks
class Cat {
meow() { console.log("Meow!"); }
}
class Dog {
bark() { console.log("Woof!"); }
}
function speak(animal: Cat | Dog) {
if (animal instanceof Cat) {
animal.meow();
} else {
animal.bark();
}
}
speak(new Cat());
speak(new Dog());
Meow! Woof!
instanceof works the same way for classes — TypeScript narrows animal to Cat inside the if, so .meow() is available, and to Dog in the else, unlocking .bark().
A custom type guard
interface Fish { swim(): void; }
interface Bird { fly(): void; }
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
}
pet is Fish is a type predicate — it tells the compiler that whenever isFish returns true, the argument should be treated as a Fish from that point on. This lets you write your own narrowing logic beyond what typeof and instanceof can check on their own.
typeof null — it returns "object", not "null", a long-standing quirk baked into JavaScript itself. A check like typeof value === "object" will happily let null through, which usually isn't what you meant. Narrow out null explicitly first (if (value !== null && typeof value === "object")) whenever a union includes it.