Difficulty: Intermediate
Type narrowing is TypeScript's ability to refine a broad type to a more specific one based on control flow analysis. The most basic form of narrowing uses the JavaScript `typeof` operator. When you write `typeof x === 'string'`, TypeScript understands that within the `true` branch, `x` is a `string`, and in the `false` branch, it is whatever remains of the original type.
The `typeof` operator returns one of these strings at runtime: `'string'`, `'number'`, `'boolean'`, `'undefined'`, `'object'`, `'function'`, `'symbol'`, and `'bigint'`. TypeScript recognizes all of these as type guards. Note that `typeof null === 'object'` is a well-known JavaScript quirk, so `typeof` alone cannot distinguish between objects and null.
Truthiness narrowing is another common pattern. When you write `if (value)`, TypeScript narrows out `null`, `undefined`, `0`, `""`, `false`, and `NaN` from the type. This is useful for quickly checking whether an optional value exists. However, truthiness narrowing can be too aggressive - it also eliminates legitimate falsy values like `0` and `""`, so use it carefully.
Equality narrowing uses `===`, `!==`, `==`, and `!=` to narrow types. When you compare a union type value to a specific literal, TypeScript narrows within the matching branch. The `==` and `!=` operators are especially useful with `null` because `x == null` checks for both `null` and `undefined`, which is often exactly what you want. These narrowing mechanisms work together to provide type safety with zero runtime overhead - they use checks you would write anyway.
function padLeft(value: string, padding: string | number): string {
if (typeof padding === "number") {
// padding is narrowed to number
return " ".repeat(padding) + value;
}
// padding is narrowed to string
return padding + value;
}
console.log(padLeft("Hello", 4));
console.log(padLeft("Hello", ">>> "));
After the typeof check, TypeScript knows padding is number in the if branch and string in the else branch, allowing you to call type-specific operations safely.
function printName(name: string | null | undefined): string {
if (name) {
// name is narrowed to string (null and undefined removed)
return name.toUpperCase();
}
return "Anonymous";
}
function getLength(value: string | string[] | null): number {
if (!value) {
return 0;
}
// value is narrowed to string | string[]
return value.length;
}
console.log(printName("alice"));
console.log(printName(null));
console.log(getLength(["a", "b", "c"]));
console.log(getLength(null));
Truthiness checks narrow out null and undefined. In getLength, the !value check handles null, and the remaining code can safely access .length on string or string[].
function compare(a: string | number, b: string | boolean): string {
if (a === b) {
// Both narrowed to string (the only common type)
return `Both are the string: ${a.toUpperCase()}`;
}
return `Different: ${a} and ${b}`;
}
function handleValue(value: string | null | undefined): string {
if (value == null) {
// Catches both null and undefined
return "No value";
}
return value.trim();
}
console.log(compare("hello", "hello"));
console.log(compare(42, true));
console.log(handleValue(null));
console.log(handleValue(" spaced "));
When a === b, TypeScript narrows both to their common type (string). The == null check catches both null and undefined in a single comparison.
typeof operator, type narrowing, truthiness narrowing, equality narrowing