We use cookies to enhance your experience on the site
CodeWorlds

Discriminated Unions - Component Variants

In space, different objects require different treatment - a planet, a star, and an asteroid are different entities with different properties. Discriminated unions allow TypeScript to distinguish object variants based on a single field.

The Problem - Different Data Shapes

Imagine a component that displays different types of cosmic objects. Each type has different fields:

1// BAD - unclear which fields are available
2interface SpaceObject {
3  type: string;
4  name: string;
5  radius?: number;
6  temperature?: number;
7  composition?: string[];
8  orbitalPeriod?: number;
9}
10
11function SpaceInfo({ obj }: { obj: SpaceObject }) {
12  // You must check each field separately
13  if (obj.radius) { /* ... */ }
14  if (obj.temperature) { /* ... */ }
15  // Easy to make mistakes!
16}

The Solution - Discriminated Unions

Instead of one interface with optional fields, we create separate interfaces connected by a common field (discriminator):

1// Each type has a common 'kind' field with a different value
2interface PlanetData {
3  kind: "planet";
4  name: string;
5  radius: number;
6  habitable: boolean;
7  moons: number;
8}
9
10interface StarData {
11  kind: "star";
12  name: string;
13  temperature: number;
14  luminosity: number;
15  spectralClass: string;
16}
17
18interface AsteroidData {
19  kind: "asteroid";
20  name: string;
21  composition: string[];
22  diameter: number;
23}
24
25// Union type - the object can be one of three types
26type SpaceObject = PlanetData | StarData | AsteroidData;

Type Narrowing

TypeScript automatically narrows the type based on the discriminator field:

1function SpaceInfo({ obj }: { obj: SpaceObject }) {
2  // Common field - available for all variants
3  const header = <h2>{obj.name}</h2>;
4
5  // Switch on the discriminator
6  switch (obj.kind) {
7    case "planet":
8      // TypeScript KNOWS that obj is PlanetData
9      return (
10        <div>
11          {header}
12          <p>Radius: {obj.radius} km</p>
13          <p>Moons: {obj.moons}</p>
14          <p>{obj.habitable ? "Habitable" : "Uninhabitable"}</p>
15        </div>
16      );
17    case "star":
18      // TypeScript KNOWS that obj is StarData
19      return (
20        <div>
21          {header}
22          <p>Temperature: {obj.temperature}K</p>
23          <p>Class: {obj.spectralClass}</p>
24        </div>
25      );
26    case "asteroid":
27      // TypeScript KNOWS that obj is AsteroidData
28      return (
29        <div>
30          {header}
31          <p>Diameter: {obj.diameter} km</p>
32          <p>Composition: {obj.composition.join(", ")}</p>
33        </div>
34      );
35  }
36}

UI Component Variants

Discriminated unions are ideal for creating UI component variants:

1// Alert variants
2type AlertProps =
3  | { variant: "success"; message: string }
4  | { variant: "error"; message: string; retryAction: () => void }
5  | { variant: "warning"; message: string; dismissable: boolean }
6  | { variant: "info"; message: string; link?: string };
7
8function Alert(props: AlertProps) {
9  const baseClass = "alert";
10
11  switch (props.variant) {
12    case "success":
13      return <div className={`${baseClass} success`}>{props.message}</div>;
14    case "error":
15      return (
16        <div className={`${baseClass} error`}>
17          {props.message}
18          <button onClick={props.retryAction}>Retry</button>
19        </div>
20      );
21    case "warning":
22      return (
23        <div className={`${baseClass} warning`}>
24          {props.message}
25          {props.dismissable && <button>Dismiss</button>}
26        </div>
27      );
28    case "info":
29      return (
30        <div className={`${baseClass} info`}>
31          {props.message}
32          {props.link && <a href={props.link}>Learn more</a>}
33        </div>
34      );
35  }
36}

Exhaustive Checking - Never Miss a Variant

1// Helper function - TypeScript will report an error if you don't handle all variants
2function assertNever(value: never): never {
3  throw new Error(`Unhandled variant: ${value}`);
4}
5
6function getLabel(obj: SpaceObject): string {
7  switch (obj.kind) {
8    case "planet": return "Planet";
9    case "star": return "Star";
10    case "asteroid": return "Asteroid";
11    default: return assertNever(obj); // error if you add a new type and don't handle it
12  }
13}
Go to CodeWorlds