Type Narrowing
Narrow union types safely using typeof, instanceof, in, and discriminated unions
Free tier: read the explanation here. Upgrade to Pro for Drills, Speed challenges & Mastery badges.
UpgradeKnowledge Debt detected
You can study this freely — but your score may plateau if these foundations have gaps. The Mastery badge requires them to be solid.
Explanation
Type narrowing is how TypeScript figures out the specific type of a value inside a conditional block. After a check, TypeScript *narrows* the union to the matching branch.
`typeof` narrowing — for primitives: ``typescript function format(val: string | number): string { if (typeof val === 'string') { return val.toUpperCase(); // TypeScript knows val is string here } return val.toFixed(2); // here val is number }
Truthiness narrowing — filters out null / undefined: ``typescript function greet(name: string | null): string { if (name) { return Hello, ${name}!; // name is string here (not null) } return 'Hello, stranger!'; }
`instanceof` narrowing — for class instances: ``typescript function logError(err: Error | string): void { if (err instanceof Error) { console.error(err.message); // err is Error } else { console.error(err); // err is string } }
`in` operator narrowing — checks if a property exists: ``typescript interface Cat { meow(): void; } interface Dog { bark(): void; } function makeSound(animal: Cat | Dog): void { if ('meow' in animal) { animal.meow(); // Cat } else { animal.bark(); // Dog } }
Discriminated unions — a shared kind/type field narrows the entire object: ``typescript interface Circle { kind: 'circle'; radius: number; } interface Rect { kind: 'rect'; width: number; height: number; } type Shape = Circle | Rect; function area(shape: Shape): number { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2; case 'rect': return shape.width * shape.height; } } The kind` literal field is the *discriminant* — TypeScript uses it to determine which variant you have.
Type predicates — user-defined type guards: ``typescript function isString(val: unknown): val is string { return typeof val === 'string'; } if (isString(someVal)) { console.log(someVal.toUpperCase()); // TypeScript knows it's string }
Examples
typeof and in narrowing
`in` narrowing is ideal when two types share some but not all properties. The discriminated union pattern (below) is cleaner when you control the type design.
type StringOrArray = string | string[];
function normalize(input: StringOrArray): string[] {
if (typeof input === 'string') {
return [input]; // input narrowed to string
}
return input; // input narrowed to string[]
}
console.log(normalize('hello')); // ['hello']
console.log(normalize(['a', 'b', 'c'])); // ['a', 'b', 'c']
// in-based narrowing for object unions
interface Admin { role: 'admin'; permissions: string[] }
interface Member { role: 'member'; planId: string }
type UserType = Admin | Member;
function describe(user: UserType): string {
if ('permissions' in user) {
return `Admin with ${user.permissions.length} permissions`; // Admin
}
return `Member on plan ${user.planId}`; // Member
}Discriminated unions
The discriminated union pattern — a shared `type` or `kind` literal field — is the most ergonomic way to handle variant types in TypeScript. It works perfectly with `switch`.
// Each variant has a literal 'type' field — the discriminant
type Result<T> =
| { type: 'success'; data: T }
| { type: 'error'; message: string; code: number };
function handleResult<T>(result: Result<T>): void {
switch (result.type) {
case 'success':
console.log('Data:', result.data); // result is { type: 'success', data: T }
break;
case 'error':
console.error(`Error ${result.code}: ${result.message}`);
break;
}
}
handleResult({ type: 'success', data: { id: 1, name: 'Alice' } });
handleResult({ type: 'error', message: 'Not found', code: 404 });How well did you understand this?
Next in TypeScript
Utility Types