🔷 TypeScript Q34 / 101

What is discriminated union?

AI-Powered Answer ✓ Answered

Discriminated unions are a powerful pattern in TypeScript that combine union types with a common, literal-typed property (the 'discriminant'). This pattern allows TypeScript to narrow down the type of an object within a union based on the value of the discriminant property, providing enhanced type safety and improved developer experience.

What is a Discriminated Union?

A discriminated union (also known as a tagged union or disjoint union) is a type formed by combining two or more distinct types into a single union type. Each member type of the union must share a common property with a unique literal type value. This shared property acts as the 'discriminant' or 'tag', enabling TypeScript's control flow analysis to determine the specific type of the object at runtime within conditional blocks.

The key components are: 1. A union type: Several types joined by the | operator. 2. A common property: All types in the union must have a property with the same name. 3. Literal types: The common property in each member type must have a unique literal type (e.g., 'circle', 'square', 1, true).

Why Use Discriminated Unions?

  • Type Safety: Ensures that you only access properties relevant to the specific member of the union, preventing runtime errors.
  • Improved Developer Experience: Provides excellent autocompletion and type-hinting in IDEs.
  • Exhaustiveness Checking: Allows the TypeScript compiler to check if you've handled all possible cases in a switch statement or if/else if chain, helping prevent bugs.
  • Clearer Code: Makes the intent of your data structures more explicit and easier to reason about.

How to Implement Discriminated Unions

To implement a discriminated union, you typically define several interfaces or types, each representing a distinct variant. Crucially, each variant must include a common property (the discriminant) whose type is a unique string or number literal. Finally, you combine these types into a union.

Example: Shapes

typescript
interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

interface Triangle {
  kind: "triangle";
  base: number;
  height: number;
}

type Shape = Circle | Square | Triangle;

In this example, kind is the discriminant property. Its value is a literal string ("circle", "square", "triangle") unique to each interface. The Shape type is a discriminated union. Now, when you work with a Shape object, TypeScript can automatically narrow its type based on the value of its kind property.

typescript
function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      // TypeScript knows 'shape' is a Circle here
      return Math.PI * shape.radius ** 2;
    case "square":
      // TypeScript knows 'shape' is a Square here
      return shape.sideLength ** 2;
    case "triangle":
      // TypeScript knows 'shape' is a Triangle here
      return 0.5 * shape.base * shape.height;
    default:
      // This case handles any unhandled variants
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

Exhaustiveness Checking

One of the most valuable features of discriminated unions is the ability to enforce exhaustiveness checking. By including a default case in a switch statement that assigns the shape variable to a never type, TypeScript will report a compilation error if you add a new variant to the Shape union but forget to handle it in the getArea function. This prevents potential runtime errors caused by unhandled cases.

Conclusion

Discriminated unions are an indispensable tool in TypeScript for modeling complex data structures that can take on different forms. By leveraging a common discriminant property, they provide robust type safety, enhance code readability, and enable powerful compile-time checks, making your applications more reliable and easier to maintain.