Programming Fundamentals › Type Systems
Type Narrowing
Refining a broad type to a specific one through checks.
Also known as: narrowing, type guards in TypeScript, control flow analysis, refining types
Type narrowing is when the compiler refines a broad type into a more specific one based on checks in your code. A value that is string | number becomes
string inside an if that checks for a string.
function format(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // here: string
}
return value.toFixed(2); // here: number
}
The compiler follows the flow of the code, and understands several kinds of check:
| Check | Narrows by |
|---|---|
typeof x === "string" | primitive type |
x instanceof Date | class |
"swim" in animal | whether a property exists |
x === null, x !== undefined | removing null and undefined (nullable types) |
if (x) | removing falsy values (truthy and falsy) |
shape.kind === "circle" | the tag of a discriminated union |
Array.isArray(x) | arrays |
function area(user: User | null) {
if (!user) return 0;
return user.age; // `null` was ruled out above
}
Custom checks: type guards
When the built-in checks aren’t enough, write a function whose return type tells the compiler what a true result means:
function isUser(x: unknown): x is User {
return typeof x === "object" && x !== null && "id" in x && "email" in x;
}
if (isUser(data)) {
data.email; // narrowed to User
}
See type guards. The compiler trusts the function, so a sloppy check is a lie that causes crashes later. For complex data, use a validation library.
Things to know
- Narrowing disappears where the compiler can’t be sure the value stays the same, such as inside a callback after the variable could have been reassigned.
Copy it into a
constfirst. - A type assertion (
x as User) isn’t narrowing. It silences the compiler without checking anything. - Avoid
any, which turns narrowing off. Preferunknown, which forces you to narrow before use (any type).