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.
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}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;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}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}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}