Introduction
Narrowing is the process by which TypeScript refines a broad type to a more specific one based on control flow analysis. When you check a value's type with typeof, instanceof, or a property check, TypeScript understands that inside the corresponding branch, the type is narrower. This is how you safely work with union types.
Key Concepts
- Type Guard: A runtime check that narrows a type within a code block.
typeofGuard: Checks primitive types ("string","number","boolean", etc.).instanceofGuard: Checks if a value is an instance of a class.inOperator Narrowing: Checks if a property exists on an object.- Truthiness Narrowing: Using truthiness checks to eliminate
null,undefined, and falsy values.
Real World Context
Every time you check if (user !== null) before accessing user.name, you are narrowing. In API handlers, you narrow response types to extract data. In event handlers, you narrow event objects to access specific properties. Narrowing is something you do constantly — TypeScript just gives it a name and formal type-level support.
Deep Dive
typeof Narrowing
The typeof operator narrows primitive types:
typescriptfunction formatValue(value: string | number | boolean): string { if (typeof value === "string") { return value.toUpperCase(); // value is string } if (typeof value === "number") { return value.toFixed(2); // value is number } return value ? "Yes" : "No"; // value is boolean }
TypeScript tracks these checks through the control flow. After the first if eliminates string, the remaining type is number | boolean.
instanceof Narrowing
For class instances, use instanceof:
typescriptclass NetworkError { constructor(public statusCode: number, public message: string) {} } class ValidationError { constructor(public field: string, public message: string) {} } function handleError(error: NetworkError | ValidationError): string { if (error instanceof NetworkError) { return `HTTP ${error.statusCode}: ${error.message}`; } return `Validation failed on ${error.field}: ${error.message}`; }
After the instanceof check, TypeScript knows the exact class and exposes its specific properties.
The in Operator
The in operator checks for property existence and narrows accordingly:
typescripttype Fish = { swim: () => void; name: string }; type Bird = { fly: () => void; name: string }; function move(animal: Fish | Bird): void { if ("swim" in animal) { animal.swim(); // animal is Fish } else { animal.fly(); // animal is Bird } }
This is particularly useful when you have object types that share some properties but differ in others.
Truthiness Narrowing
Checking for truthiness eliminates null, undefined, 0, "", and false:
typescriptfunction greetUser(name: string | null | undefined): string { if (name) { return `Hello, ${name}!`; // name is string (non-empty) } return "Hello, guest!"; }
Be careful: truthiness narrowing eliminates all falsy values, including valid ones like 0 or "".
Equality Narrowing
Strict equality checks also narrow types:
typescriptfunction example(x: string | number, y: string | boolean): void { if (x === y) { // x and y must both be string (the only overlapping type) console.log(x.toUpperCase()); // x is string console.log(y.toUpperCase()); // y is string } }
TypeScript deduces that if x === y and they share only string as a common type, both must be strings.
Common Pitfalls
- Truthiness narrowing eliminates valid falsy values — Checking
if (count)will treat0as falsy. Useif (count !== null && count !== undefined)when0is a valid value. typeof nullreturns"object"— This is a JavaScript quirk. Checkingtypeof value === "object"does not eliminatenull. Always add a null check:value !== null && typeof value === "object".
Best Practices
- Prefer
typeoffor primitives andinstanceoffor classes — Use the narrowing technique that matches the type you are checking. - Use the
inoperator for discriminating object shapes — When classes are not available (e.g., plain objects from APIs),inis the cleanest approach.
Summary
typeofnarrows primitive types;instanceofnarrows class instances.- The
inoperator narrows based on property existence. - Truthiness checks eliminate
nullandundefinedbut also other falsy values. - TypeScript's control flow analysis tracks narrowing through if/else, switch, and ternary expressions.
Code Examples
// Combining multiple narrowing techniques
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number };
function getArea(shape: Shape): number {
if ("radius" in shape) {
// Narrowed to circle
return Math.PI * shape.radius ** 2;
}
// Narrowed to rectangle
return shape.width * shape.height;
}
console.log(getArea({ kind: "circle", radius: 5 })); // 78.54
console.log(getArea({ kind: "rectangle", width: 10, height: 3 })); // 30