Type Narrowing in TypeScript
How TypeScript refines a wider type to a more specific one inside conditional checks.
2 min read
Narrowing is how TypeScript takes a broader type — usually a union — and, inside a conditional check, works out that within a specific branch of code, the value must actually be a more specific type. It's what makes working with union types safe rather than requiring manual type assertions everywhere.
typeof guards
The most common narrowing tool, for distinguishing between primitive types:
function printLength(value: string | number) {
if (typeof value === "string") {
console.log(value.length); // narrowed to string here
} else {
console.log(value.toFixed(2)); // narrowed to number here
}
}Inside the if block, TypeScript knows value can only be a string — checking typeof is enough for it to safely allow .length. In the else block, by elimination, it must be number.
instanceof guards
For narrowing between classes:
class Dog { bark() { console.log("Woof"); } }
class Cat { meow() { console.log("Meow"); } }
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // narrowed to Dog
} else {
animal.meow(); // narrowed to Cat
}
}in guards, for checking a property's presence
Useful when the union members aren't easily told apart by typeof or instanceof — for example, two plain object shapes:
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; sideLength: number };
function area(shape: Circle | Square) {
if ("radius" in shape) {
return Math.PI * shape.radius ** 2; // narrowed to Circle
}
return shape.sideLength ** 2; // narrowed to Square
}Discriminated unions: the pattern worth knowing by name
Giving every member of a union a shared literal property (like kind above) and checking it is called a discriminated union — it's the most reliable, scalable way to narrow between several object shapes:
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; sideLength: number };
type Shape = Circle | Square;
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
}
}Checking shape.kind in the switch narrows shape to exactly the right member in each case — and if a third shape is ever added to the union, TypeScript's exhaustiveness checking (with strict mode and a never-typed default case) can catch a forgotten case at compile time, before it ever ships as a bug.
Truthiness narrowing
function printName(name?: string) {
if (name) {
console.log(name.toUpperCase()); // narrowed away from undefined
}
}A plain truthiness check narrows away undefined, null, and other falsy values — the same everyday pattern from JavaScript, but one TypeScript actually understands and reflects in the resulting type.
Narrowing is what makes union types practical to work with day to day. Next, enum — a dedicated construct for a fixed, named set of values, and how it compares to the string-literal unions used throughout this lesson.