Narrowing is how you work with unions safely; it is the day-to-day skill that makes TypeScript feel natural.
Narrowing is the checker following your code's control flow and refining a variable's type after each check. Inside if (typeof x === "string") { ... } a string | number is just string.
The checks TypeScript understands:
- `typeof`:
"string","number","boolean","bigint","symbol","undefined","function","object". Beware:typeof null === "object", and TypeScript knows that — atypeof x === "object"check leavesnullin the type. - Truthiness:
if (value)removesnullandundefined(and also falsy members like""and0— be careful with numbers and strings). - Equality:
x === "left",x !== null, andswitchon a literal union. - `in`:
"swim" in animalkeeps the members that have (or may have) aswimproperty. - `instanceof`:
err instanceof Errornarrows to the class type. - `Array.isArray(x)` narrows to the array member.
Narrowing works with early exits too: after if (typeof x === "string") return;, the rest of the function knows x is a number. Assignments narrow as well — after x = 5 the checker knows x is a number until it changes again.
The checker narrows variables and parameters, not arbitrary expressions. Narrowing of a property like obj.value is lost after a function call that might have changed it, and narrowing inside callbacks may be reset for let variables because the callback could run later. Copying to a const first (const v = obj.value) usually fixes such cases.
When narrowing can't express what you need, you can write your own checks as type predicates — a later lesson.
Example
type Shape = { kind: "circle"; radius: number } | { kind: "rect"; w: number; h: number };
function format(value: string | number | Date | null): string {
if (value === null) return "-";
if (typeof value === "number") return value.toFixed(2); // number
if (value instanceof Date) return value.toISOString(); // Date
return value.trim(); // string
}
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2; // circle member
case "rect": return s.w * s.h; // rect member
}
}
function speak(pet: { bark(): string } | { meow(): string }): string {
return "bark" in pet ? pet.bark() : pet.meow();
}
console.log(format(3.14159), format(" hi "), format(null), area({ kind: "rect", w: 2, h: 3 }));
console.log(speak({ meow: () => "meow" }));Remember
- Checks like
typeof,===,in,instanceofnarrow unions automatically - Early returns narrow the rest of the function
typeof x === "object"does not exclude null
Watch out for
- Using a truthiness check on
number | undefinedand accidentally treating 0 as missing.