1. Introduction
A discriminated union (also called a tagged union or algebraic data type) is a union of object types that all share a common property — the discriminant, or tag — whose type is a distinct literal for each member. Because the tag's literal value uniquely identifies which member of the union you're dealing with, TypeScript's control-flow analysis can narrow the union to the exact matching interface inside a switch on the tag or an if that checks it, giving you full IntelliSense and type safety for the narrowed member's other properties without any type assertions.
Cricket analogy: A scorecard's dismissal type field ('bowled', 'caught', 'run out') tells you exactly which follow-up details apply, just as a discriminant like kind: "circle" lets TypeScript narrow a union to the exact matching shape.
2. Syntax
interface Circle {
kind: "circle"; // literal discriminant
radius: number;
}
interface Square {
kind: "square";
side: number;
}
type Shape = Circle | Square;
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // shape: Circle
case "square":
return shape.side ** 2; // shape: Square
}
}3. Explanation
The discriminant property (kind in the example) must be typed as a distinct literal type on every member of the union — "circle" on Circle, "square" on Square, and so on — not as the wider string. When you switch or branch on shape.kind, TypeScript's narrowing algorithm intersects the checked literal with each union member's declared literal for that property; only the member(s) whose kind literal matches remain, so inside case "circle": the compiler knows shape is exactly Circle and grants access to radius without a cast.
Cricket analogy: Just as an umpire's dismissal call must be one exact literal like 'lbw' rather than a vague 'out', the kind field on Circle must be the literal "circle", not the wider string, for narrowing to work in a switch.
This pattern scales cleanly to any number of union members and is the idiomatic TypeScript equivalent of algebraic data types / pattern matching found in functional languages. It is heavily used for representing API responses ({ status: "success", data } | { status: "error", message }), Redux-style actions ({ type: "INCREMENT" } | { type: "SET", value: number }), and state machines.
Cricket analogy: Just as a dismissal-type field scales to cover every possible way a batsman gets out across formats, a discriminated union scales to any number of members, powering things like ball-by-ball commentary state machines.
Adding a default case that assigns the still-unhandled value to a variable typed never (const _exhaustive: never = shape;) turns a missing case into a compile-time error: if a new member is added to the union later and a case is forgotten, TypeScript will report that the new member's type isn't assignable to never, catching the omission before deployment.
Gotcha: if the discriminant property is typed as string instead of a specific literal (e.g. kind: string rather than kind: "circle"), TypeScript cannot narrow on it at all, because string doesn't distinguish members. This most often happens accidentally when the interface is inferred from a non-const value, or when someone widens the type while refactoring.
4. Example
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
side: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Circle | Square | Rectangle;
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
case "rectangle":
return shape.width * shape.height;
default:
const _exhaustive: never = shape;
return _exhaustive;
}
}
const shapes: Shape[] = [
{ kind: "circle", radius: 2 },
{ kind: "square", side: 3 },
{ kind: "rectangle", width: 4, height: 5 },
];
shapes.forEach((s) => console.log(s.kind, area(s).toFixed(2)));5. Output
circle 12.57
square 9.00
rectangle 20.006. Key Takeaways
- A discriminated union is a union of object types sharing a common literal-typed 'tag' property.
- Switching or branching on the tag lets TypeScript narrow to the exact matching interface, with no casts needed.
- The tag property must be a distinct literal type per member, not a wide
string, for narrowing to work. - A
never-typed exhaustiveness check in the default/else branch catches unhandled union members at compile time. - This pattern is TypeScript's idiomatic equivalent of algebraic data types / pattern matching.
Practice what you learned
1. What property must every member of a discriminated union share for narrowing to work correctly?
2. Gotcha: what happens if the discriminant property is typed as `string` instead of a specific literal like `"circle"`?
3. What is the purpose of assigning the switch's default-case value to a variable typed `never`?
4. In the Example, what is the narrowed type of `shape` inside `case "rectangle":`?
5. Which of these is a typical real-world use of discriminated unions?
Was this page helpful?
You May Also Like
Type Guards in TypeScript
Narrow union types safely within conditional blocks using `typeof`, `instanceof`, and custom user-defined type predicates with `is`.
Union and Intersection Types in TypeScript
Combine types with `|` (union) and `&` (intersection) to model values that can be one of several shapes or must satisfy all of them.
interface vs type in TypeScript
Compare TypeScript's `interface` and `type` keywords — when each is preferable, and what each can and cannot express.
Literal Types in TypeScript
Narrow a type down to one exact value -- a specific string, number, or boolean -- and combine literals into precise unions.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics