Unions and narrowing
A value that is one of several things, and how to prove which one it is.
After this lesson you can
- Declare a value that may be one of several types
- Narrow a union with typeof, in and a discriminant
- Use a never check to catch a case you forgot
A union says a value is one of several types.
type Id = string | number;
Until you prove which one it is, you may only do what both allow. That is not the compiler being awkward: at runtime the value really could be either.
Narrowing
A check the compiler understands narrows the type inside that branch.
function label(id: string | number): string {
if (typeof id === "string") return id.toUpperCase(); // string here
return id.toFixed(0); // number here
}
typeof, instanceof, in, Array.isArray and a plain comparison against
a literal all narrow.
Discriminated unions
The pattern worth knowing by name. Give every member a shared literal field, and checking that field narrows the whole object:
type Result =
| { status: "ok"; value: number }
| { status: "failed"; error: string };
function show(r: Result): string {
if (r.status === "ok") return `got ${r.value}`;
return `failed: ${r.error}`; // r.value is not even offered here
}
This is how you model a thing that either worked or did not, without an optional field that is sometimes set and sometimes not.
Try it
type Result = | { status: "ok"; value: number } | { status: "failed"; error: string }; function show(r: Result): string { switch (r.status) { case "ok": return `got ${r.value}`; case "failed": return `failed: ${r.error}`; default: { const impossible: never = r; throw new Error(`unhandled: ${String(impossible)}`); } }} export function run(): string[] { return [ show({ status: "ok", value: 42 }), show({ status: "failed", error: "timeout" }), ];}Catching the case you forgot
Assigning the narrowed value to never at the end makes the compiler fail
when someone adds a new member to the union and forgets this switch:
default: {
const impossible: never = r;
throw new Error(`unhandled: ${impossible}`);
}
Add { status: "pending" } to the union and that line stops compiling. That
is the compiler finding, at build time, every place a new case needs
handling.
Try it yourself
2 visible tests · 2 hidden testsA Shape is either { kind: "circle", radius: number } or
{ kind: "rectangle", width: number, height: number }. Implement
area(shape: Shape): number. Narrow on kind — do not reach for a cast.
Round the circle's area to two decimal places.
area({"kind":"rectangle","width":4,"height":5})area({"kind":"circle","radius":2})
Sign up to check the hidden tests and save your progress. Sign up