What are TypeScript Discriminated Unions?

TypeScript discriminated unions, also known as tagged unions, are a powerful feature enabling developers to model complex data structures with clarity and type safety. These unions involve creating a common property, known as a discriminator, which serves to distinguish between different shapes or types within the union.

By relying on discriminators, TypeScript enables precise type inference and facilitates exhaustive checks, reducing the risk of runtime errors. Discriminated unions are widely used to express various scenarios, such as handling different shapes of objects or representing a set of string literals, enhancing code readability and robustness.

Basic Discriminated Union

interface Square { kind: "square"; size: number; } interface Circle { kind: "circle"; radius: number; } type Shape = Square Circle; function area(shape: Shape): number { switch (shape.kind) { case "square": return shape.size * shape.size; case "circle": return Math.PI * shape.radius * shape.radius; } } const square: Shape = { kind: "square", size: 5 }; const circle: Shape = { kind: "circle", radius: 3 }; console.log(area(square)); // Output: 25 console.log(area(circle)); // Output: ~28.27

Running the TypeScript compiler (tsc) will generate the following JavaScript code:

function area(shape) { switch (shape.kind) { case "square": return shape.size * shape.size; case "circle": return Math.PI * shape.radius * shape.radius; } } const square = { kind: "square", size: 5 }; const circle = { kind: "circle", radius: 3 }; console.log(area(square)); // Output: 25 console.log(area(circle)); // Output: ~28.27

Discriminated Union with Literal Types

type Status = "success" "error"; interface ApiResponse { status: Status; data: object; } function handleResponse(response: ApiResponse): void { switch (response.status) { case "success": console.log("Request succeeded!"); break; case "error": console.log("Error occurred:", response.data); break; } } const successResponse: ApiResponse = { status: "success", data: { message: "Data retrieved." } }; const errorResponse: ApiResponse = { status: "error", data: { message: "Invalid request." } }; handleResponse(successResponse); // Output: Request succeeded! handleResponse(errorResponse); // Output: Error occurred: { message: "Invalid request." }

Running the TypeScript compiler (tsc) will generate the following JavaScript code:

function handleResponse(response) { switch (response.status) { case "success": console.log("Request succeeded!"); break; case "error": console.log("Error occurred:", response.data); break; } } const successResponse = { status: "success", data: { message: "Data retrieved." } }; const errorResponse = { status: "error", data: { message: "Invalid request." } }; handleResponse(successResponse); // Output: Request succeeded! handleResponse(errorResponse); // Output: Error occurred: { message: "Invalid request." }

Adding Discriminant to Existing Types

interface Car { type: "car"; brand: string; } interface Bicycle { type: "bicycle"; color: string; } type Vehicle = Car Bicycle; function displayVehicleInfo(vehicle: Vehicle): void { switch (vehicle.type) { case "car": console.log(`Car - Brand: ${vehicle.brand}`); break; case "bicycle": console.log(`Bicycle - Color: ${vehicle.color}`); break; } } const myCar: Vehicle = { type: "car", brand: "Toyota" }; const myBicycle: Vehicle = { type: "bicycle", color: "Blue" }; displayVehicleInfo(myCar); // Output: Car - Brand: Toyota displayVehicleInfo(myBicycle); // Output: Bicycle - Color: Blue

Running the TypeScript compiler (tsc) will generate the following JavaScript code:

function displayVehicleInfo(vehicle) { switch (vehicle.type) { case "car": console.log(`Car - Brand: ${vehicle.brand}`); break; case "bicycle": console.log(`Bicycle - Color: ${vehicle.color}`); break; } } const myCar = { type: "car", brand: "Toyota" }; const myBicycle = { type: "bicycle", color: "Blue" }; displayVehicleInfo(myCar); // Output: Car - Brand: Toyota displayVehicleInfo(myBicycle); // Output: Bicycle - Color: Blue

Conclusion

Discriminated unions provide a way to model complex data scenarios in a type-safe manner, ensuring that the TypeScript compiler can accurately infer and narrow down types based on discriminant properties. This results in code that is both expressive and resilient, as it facilitates pattern matching and exhaustive checks, reducing the likelihood of runtime errors.