Skip to main content

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

javascript
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

javascript
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.

ConditionExplanation
1All members of the union (union) must be object types
2All of them must share a key with the same name (for example, kind, type, status)
3That key must have a literal value ("circle", "square", "error"), not just string

Example:

javascript
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):

javascript
type Bad = { status: string; data?: string }; type Worse = { status: string; message?: string }; type InvalidUnion = Bad | Worse; // TS cannot narrow by status

Example with switch and an exhaustiveness check

javascript
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.

NameExample 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

javascript
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

javascript
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:

javascript
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:

javascript
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

ConceptDescription
Discriminant propertyA property shared by all members of a union type that has a unique literal value
Why it is neededSo that TypeScript can automatically narrow the type within a union
Common nameskind, type, status, variant, tag
Key roleMakes it possible to build "discriminated unions" and write safe switch branches
Main conditionThe property's values must be literal and unique

A simple comparison:

A discriminant property is like an object's "passport": TypeScript looks at the kind field and immediately knows which "citizenship" (type) it belongs to.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.