Narrowing is how TypeScript refines a broad type into a more specific one by following your control flow : if, equality checks, loops, and so on. (Source: Narrowing.)
Why it matters
function padLeft(padding: number | string, input: string): string {
return " ".repeat(padding) + input;
// Error: Argument of type 'string | number' is not assignable to parameter of type 'number'.
}
padding is a union (number | string), but String.repeat only accepts a number. TypeScript blocks the call because one branch of the union is unsafe. You must check the type before using it.
function padLeft(padding: number | string, input: string): string {
if (typeof padding === "number") {
return " ".repeat(padding) + input; // padding is number here
}
return padding + input; // padding is string here
}
That typeof check is a type guard. Inside the if branch, TypeScript knows padding is a number. In the else path it is a string. The union has been narrowed to one concrete type per branch.
typeof type guards
typeof returns one of: "string", "number", "bigint", "boolean", "symbol", "undefined", "object", "function".
Watch out: typeof null === "object" in JavaScript.
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
// strs is string[] | null : still might be null!
for (const s of strs) {
// Error without a null check: 'strs' is possibly 'null'.
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
Here typeof strs === "object" is not enough. Arrays are objects, but so is null, so TypeScript still treats strs as string[] | null in that branch. You need an extra null check before looping.
Truthiness narrowing
Values like 0, "", null, undefined, and NaN are falsy. Checks like if (strs) narrow away those values : but can also skip empty strings:
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
strs && typeof strs === "object" first drops falsy values (including null), then confirms you have an object. That makes the for...of safe. Prefer explicit !== null when empty string is a valid value you still want to handle.
Equality narrowing
=== / !== (and switch) narrow types too:
function example(x: string | number, y: string | boolean) {
if (x === y) {
// both must be string
x.toUpperCase();
y.toLowerCase();
}
}
The only type that exists in both unions is string. So when x === y, TypeScript narrows both variables to string and string methods become valid.
in operator and instanceof
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim();
} else {
animal.fly();
}
}
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
} else {
console.log(x.toUpperCase());
}
}
"swim" in animal checks for a property name at runtime and narrows to Fish when it exists. instanceof Date checks the prototype chain and narrows to Date. The else branches get the remaining type (Bird or string).
Discriminated unions
Give each variant a shared literal field (a discriminant):
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; sideLength: number };
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
}
}
kind is the discriminant. After case "circle", TypeScript knows shape is the circle variant and exposes radius. After case "square", only sideLength is available. This is the safest pattern for modeling related shapes with different fields.
Type predicates
Write your own type guard with value is Type:
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
const pet: Fish | Bird = { swim() {} };
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
The return type pet is Fish is a type predicate. When isFish(pet) is true, TypeScript treats pet as Fish in that branch. When false, it treats pet as Bird. Use predicates when built-in guards (typeof, in, instanceof) are not enough.
Practice
- Implement
padLeftwith atypeofguard. - Fix
printAllsonulland empty string are both handled intentionally. - Model a
Resultas{ ok: true; value: string } | { ok: false; error: string }and narrow onok.