Discriminated unions
| One literal tag | |
| Switch on the tag | |
| Never miss a case | |
| Shape API responses |
One literal tag
A discriminated union is a union where every member carries the same tag field with a different literal value. The tag tells TypeScript which shape it is looking at.
Here Circle and Square share the kind field, so Shape is either a circle or a square and nothing else.
type Circle = {
kind: "circle";
radius: number;
};
type Square = {
kind: "square";
size: number;
};
type Shape = Circle | Square;
Switch on the tag
Narrowing means TypeScript shrinks the union after you test the tag. Inside each case branch only the matching member remains, so its own fields are safe to read.
Checking shape.kind once is enough for the whole switch body, and the const box at the end proves the call returns 16.
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return 3.14 * shape.radius * shape.radius;
case "square":
return shape.size * shape.size;
}
}
const box: Shape = { kind: "square", size: 4 };
console.log(area(box)); // 16
Never miss a case
A switch that forgets a member is a classic bug. The assertNever helper turns a forgotten case into a compile error instead of a silent wrong answer.
In the default branch shape has type never when every case is covered. If a new member is added later, shape stops being never and the compiler points at the exact line to fix.
function assertNever(value: never): never {
throw new Error("Unknown shape");
}
function total(shape: Shape): number {
switch (shape.kind) {
case "circle":
return 3.14 * shape.radius * shape.radius;
case "square":
return shape.size * shape.size;
default:
return assertNever(shape);
}
}
Shape API responses
API responses fit the same pattern perfectly. Success and failure share a status tag, so the reader always knows which fields exist.
Test res.status once and TypeScript narrows res for you. No casts and no guessing about missing fields.
type ApiOk = {
status: "ok";
items: string[];
};
type ApiFail = {
status: "error";
message: string;
};
type ApiResult = ApiOk | ApiFail;
function show(res: ApiResult): string {
if (res.status === "ok") {
return res.items.join(", ");
}
return res.message;
}
Next: Intersections
Article author: Arthur Isaev