TypeScript Type Narrowing
A developer reference for narrowing union types using typeof, instanceof, in, and discriminated unions.
TL;DR
- Narrow primitive unions with JavaScript
typeofconditional checks. - Distinguish class instances safely using the
instanceofoperator. - Build custom predicate functions using the
value is Typesyntax.
Typeof and Truthiness
typeof primitive guardNarrows unions containing primitive strings, numbers, or booleans.
function pad(val: string | number) {
if (typeof val === 'string') {
return val.padStart(4, '0');
}
return val.toFixed(2);
}truthiness checkFilters out null, undefined, false, and empty strings.
function printAll(strs: string[] | null) {
if (strs && strs.length > 0) {
for (const s of strs) console.log(s);
}
}equality narrowingUses strict equality checks to eliminate candidate types.
function compare(
x: string | number, y: string | boolean
) {
if (x === y) {
console.log(x.toUpperCase()); // x is string
}
}Instanceof and In Operator
instanceof checkVerifies prototype inheritance against known class constructors.
function handleError(err: unknown) {
if (err instanceof Error) {
console.error(err.stack);
}
}in operator guardTests whether an object possesses a specific property key.
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ('swim' in animal) return animal.swim();
animal.fly();
}Array.isArray guardDistinguishes array collections from single element items.
function ensureList(
input: string | string[]
): string[] {
if (Array.isArray(input)) return input;
return [input];
}Discriminated Unions
discriminant propertyShares a common literal tag to distinguish object variants.
type Circle = { kind: 'circle'; radius: number };
type Square = { kind: 'square'; size: number };
type Shape = Circle | Square;switch narrowingEvaluates the shared discriminant across case branches.
function area(s: Shape): number {
switch (s.kind) {
case 'circle': return Math.PI * s.radius ** 2;
case 'square': return s.size * s.size;
}
}exhaustive checkUses never to catch unhandled union members at compile time.
function assertNever(x: never): never {
throw new Error(`Unhandled variant: ${x}`);
}Custom Type Predicates
is type predicateReturns a boolean that narrows the parameter in calling scopes.
interface Cat { meow: () => void; }
function isCat(pet: unknown): pet is Cat {
return (pet as Cat)?.meow !== undefined;
}filter predicateRemoves null and undefined from array sequences cleanly.
function isNonNullable<T>(
val: T
): val is NonNullable<T> {
return val !== null && val !== undefined;
}
const clean = [1, null, 2].filter(isNonNullable);assertion functionGuarantees a type or throws an explicit runtime exception.
function assertIsString(
val: unknown
): asserts val is string {
if (typeof val !== 'string') {
throw new TypeError('Expected string');
}
}Tips
- Leverage discriminated unions with literal
kindtags to achieve exhaustive compile-time checking in switch statements. - Use user-defined type guard predicates when filtering nullable items out of arrays with the
filter()method.
Warnings
- Remember that
typeof nullevaluates to object, so always check for null before inspecting object properties. - Custom assertion functions using
asserts conditionthrow at runtime, so never pass unvalidated external data directly.
In Practice
Uses discriminated unions and exhaustive type narrowing to process diverse checkout payment responses.
- Declare typed outcome payloads for card, bank transfer, and failure states.
- Combine the payloads into a discriminated union with a status tag.
- Implement an exhaustive switch statement with a never fallback guard.
- Execute transaction processing with guaranteed compile-time coverage.
type Card = { kind: 'card'; id: string; sum: number };
type Wire = { kind: 'wire'; ref: string; sum: number };
type Pay = Card | Wire;
function pay(p: Pay): string {
switch (p.kind) {
case 'card': return `Card ${p.id}: $${p.sum}`;
case 'wire': return `Wire ${p.ref}: $${p.sum}`;
default: {
const _check: never = p;
return _check;
}
}
}FAQ
TypeScript analyzes your program reachable code paths based on runtime checks like if, switch, and early returns. When the compiler encounters a check like typeof x === 'string', it filters out impossible union branches in subsequent statements.
Use instanceof when verifying instances of concrete classes that exist at runtime. Use the in operator when checking for property existence on plain JavaScript object literals or interfaces that have no runtime constructor.
if ('message' in error) console.log(error.message);An exhaustive check assigns an unhandled switch case to a variable typed as never. If you add a new member to the union without updating the switch branches, the TypeScript compiler immediately flags a type error at compile time.
A standard boolean function returns boolean, which tells TypeScript nothing about parameter shapes. A custom type guard declares a type predicate return type like pet is Fish, prompting the compiler to refine the variable type in calling scopes.