What is type narrowing in TypeScript?
Type narrowing is how TypeScript refines a broad type to a more specific one inside conditional blocks using typeof, instanceof, in, or custom type guards.
TypeScript
Overview
When a variable has a broad type (like `string | number` or `unknown`), TypeScript cannot safely let you call string-only or number-only methods on it. Type narrowing is the process of writing checks that let TypeScript prove which specific type is in use inside a code block.
Key Points
- •`typeof x === "string"` narrows to `string`; works for `number`, `boolean`, `symbol`, `bigint`, `function`, `undefined`.
- •`instanceof x` narrows to a class instance type.
- •`"key" in obj` narrows to types that have that key.
- •Assignment and truthiness checks also narrow types.
- •Custom type guard functions: `function isUser(x: unknown): x is User { ... }` - the return type `x is User` tells TypeScript what to narrow to.
- •The `never` type represents an unreachable code path useful for exhaustive checks.
Examples
typeof narrowing
typescriptfunction format(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase(); // TS knows: string
}
return value.toFixed(2); // TS knows: number
}instanceof narrowing
typescriptfunction logError(error: unknown) {
if (error instanceof Error) {
console.error(error.message); // TS knows: Error
console.error(error.stack);
} else {
console.error('Unknown error:', error);
}
}in operator narrowing (discriminated union)
typescripttype Circle = { kind: 'circle'; radius: number };
type Square = { kind: 'square'; side: number };
type Shape = Circle | Square;
function area(shape: Shape): number {
if (shape.kind === 'circle') {
return Math.PI * shape.radius ** 2; // TS knows: Circle
}
return shape.side ** 2; // TS knows: Square
}Custom type guard function
typescriptinterface ApiUser { id: number; name: string; email: string; }
function isApiUser(value: unknown): value is ApiUser {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
const data: unknown = await response.json();
if (isApiUser(data)) {
console.log(data.name); // safely typed as ApiUser
}Exhaustiveness check with never
typescripttype Shape = Circle | Square | Triangle;
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'square': return shape.side ** 2;
case 'triangle': return (shape.base * shape.height) / 2;
default:
const _exhaustive: never = shape; // Error if a new shape isn't handled
throw new Error('Unhandled shape');
}
}Pro Tip
Use a discriminant field (`kind`, `type`, `__typename`) on union members - the literal type pattern is the most readable and efficient narrowing strategy. The `switch` on `kind` makes adding a new variant a compile error (via `never`) instead of a runtime bug.