Discriminant property
What is a discriminant property
A discriminant property is a shared property that is present on every member of a union (union) and takes unique literal values, letting TypeScript distinguish one type variant from another.
In simpler terms: a discriminant property is a "tag" that tells the compiler what it is looking at.
Example
type Circle = {
kind: "circle";
radius: number;
};
type Square = {
kind: "square";
side: number;
};
type Shape = Circle | Square;Here the kind property is the discriminant property.
It:
- exists on both types,
- takes unique literal values (
"circle","square"), - and is exactly what TypeScript uses to tell which type is currently in use.
Usage
function area(shape: Shape) {
if (shape.kind === "circle") {
// shape: Circle
return Math.PI * shape.radius ** 2;
} else {
// shape: Square
return shape.side ** 2;
}
}TypeScript sees that kind is the discriminant, and it automatically narrows the type (narrowing) in each branch.
How TypeScript "recognizes" the discriminant
For a property to count as discriminant, three conditions must hold.
| Condition | Explanation |
|---|---|
| 1 | All members of the union (union) must be object types |
| 2 | All of them must share a key with the same name (for example, kind, type, status) |
| 3 | That key must have a literal value ("circle", "square", "error"), not just string |
Example:
type Loading = { status: "loading" };
type Success = { status: "success"; data: string };
type Error = { status: "error"; message: string };
type Response = Loading | Success | Error;Example (will not be discriminated):
type Bad = { status: string; data?: string };
type Worse = { status: string; message?: string };
type InvalidUnion = Bad | Worse; // TS cannot narrow by statusExample with switch and an exhaustiveness check
function handle(response: Response) {
switch (response.status) {
case "loading":
return "Loading...";
case "success":
return `Data: ${response.data}`;
case "error":
return `Error: ${response.message}`;
default:
const _exhaustive: never = response;
return _exhaustive;
}
}TypeScript knows that status is the discriminant, and it will automatically narrow the type in every case. If you add a new variant but do not handle it, the compiler will warn you about the error through never.
Common discriminant names
You can name the property however you like, but in practice these are the ones used most often.
| Name | Example value |
|---|---|
kind | "circle", "square" |
type | "add", "remove", "reset" |
status | "loading", "success", "error" |
variant | "light", "dark" |
tag | "left", "right" |
mode | "auto", "manual" |
Real-world use cases
Redux-like actions
type Action =
| { type: "add"; payload: number }
| { type: "remove"; id: string }
| { type: "reset" };
function reducer(state: number[], action: Action) {
switch (action.type) {
case "add":
return [...state, action.payload];
case "remove":
return state.filter((_, i) => i.toString() !== action.id);
case "reset":
return [];
}
}type is the discriminant property.
Loading states
type State =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string }
| { status: "error"; error: string };
function render(state: State) {
if (state.status === "loading") return "Loading...";
if (state.status === "error") return `${state.error}`;
if (state.status === "success") return `${state.data}`;
return "Idle";
}status is the discriminant property.
Why this matters so much
Without a discriminant, TypeScript has no way to tell the members of a union apart:
type A = { name: string };
type B = { age: number };
type AB = A | B;
function f(v: AB) {
if ("name" in v) { // TS cannot say for certain that v is A
console.log(v.name);
}
}With a discriminant, everything becomes strictly typed and safe:
type A = { type: "A"; name: string };
type B = { type: "B"; age: number };
type AB = A | B;
function f(v: AB) {
if (v.type === "A") {
console.log(v.name); // TS knows that v is A
}
}Summary
| Concept | Description |
|---|---|
| Discriminant property | A property shared by all members of a union type that has a unique literal value |
| Why it is needed | So that TypeScript can automatically narrow the type within a union |
| Common names | kind, type, status, variant, tag |
| Key role | Makes it possible to build "discriminated unions" and write safe switch branches |
| Main condition | The property's values must be literal and unique |
A simple comparison:
A discriminant property is like an object's "passport": TypeScript looks at the
kindfield and immediately knows which "citizenship" (type) it belongs to.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.