- TypeScript Tutorial
- TypeScript Home
- TypeScript Introduction
- TypeScript Setup
- TypeScript First Program
- TypeScript vs JavaScript
- TypeScript Data Types
- TypeScript Type Inference
- TypeScript Type Annotations
- TypeScript Interfaces
- TypeScript Enums
- TypeScript Type Aliases
- TypeScript Type Assertions
- TypeScript Variables
- TypeScript Functions
- TypeScript Functions
- TypeScript Optional Parameters
- TypeScript Default Parameters
- TypeScript Rest Parameters
- TypeScript Arrow Functions
- Classes and Objects
- Introduction to Classes
- Properties and Methods
- Access Modifiers
- Static Members
- Inheritance
- Abstract Classes
- Interfaces vs Classes
- Advanced Types
- TypeScript Union Types
- TypeScript Intersection Types
- TypeScript Literal Types
- TypeScript Nullable Types
- TypeScript Type Guards
- TypeScript Discriminated Unions
- TypeScript Index Signatures
- TypeScript Generics
- Introduction to Generics
- TypeScript Generic Functions
- TypeScript Generic Classes
- TypeScript Generic Constraints
- TypeScript Modules
- Introduction to Modules
- TypeScript Import and Export
- TypeScript Default Exports
- TypeScript Namespace
- Decorators
- Introduction to Decorators
- TypeScript Class Decorators
- TypeScript Method Decorators
- TypeScript Property Decorators
- TypeScript Parameter Decorators
- Configuration
- TypeScript tsconfig.json File
- TypeScript Compiler Options
- TypeScript Strict Mode
- TypeScript Watch Mode
Discriminated Unions in TypeScript
Discriminated unions (also known as tagged unions or algebraic data types) are one of TypeScript's most powerful features for modeling complex data. They allow you to define a type that could be one of several different shapes, using a single "tag" or "discriminator" to help TypeScript identify exactly which one you are working with. If you've ever struggled with checking if a property exists on an object before using it, discriminated unions are the solution you've been looking for.
What are Discriminated Unions?
A discriminated union is a union type where every member shares a common property—the discriminator. This property must be a "literal type" (like a specific string "success" or "error", rather than just string). When you check this property in your code using an if or switch statement, TypeScript’s "Control Flow Analysis" kicks in. It realizes that if the tag matches a specific value, the object must belong to that specific interface, effectively "unlocking" the properties unique to that type.
string instead of a specific value like "circle", TypeScript won't be able to narrow the type properly.
Key Concepts
- Discriminator Property: A shared property across all types in the union that uses a unique literal value (string, number, or boolean).
- Union Type: Combining multiple interfaces or types into one using the pipe (
|) symbol. - Type Narrowing: The process where TypeScript identifies the specific type within a union based on your logic, ensuring type safety.
Example of Discriminated Unions
Imagine you are building a graphic design tool. You need to handle different shapes, but each shape has different dimensions (a circle has a radius, while a square has a side length). Without discriminated unions, you might end up with a messy object full of optional properties.
interface Circle {
type: "circle"; // The Discriminator
radius: number;
}
interface Square {
type: "square"; // The Discriminator
sideLength: number;
}
// The Discriminated Union
type Shape = Circle | Square;
function calculateArea(shape: Shape): number {
// TypeScript knows 'type' exists on both interfaces
if (shape.type === "circle") {
// Inside this block, TypeScript knows 'shape' is a Circle
return Math.PI * shape.radius * shape.radius;
} else {
// Inside this block, TypeScript knows 'shape' must be a Square
return shape.sideLength * shape.sideLength;
}
}
const myCircle: Circle = { type: "circle", radius: 10 };
const mySquare: Square = { type: "square", sideLength: 5 };
console.log(calculateArea(myCircle)); // Output: 314.159
console.log(calculateArea(mySquare)); // Output: 25
Explanation:
- The
Shapetype is the union. It says "A shape is either a Circle OR a Square." - Both interfaces have the
typeproperty. This is our "tag." - In
calculateArea, when we checkshape.type === "circle", TypeScript automatically hides thesideLengthproperty and reveals theradiusproperty because it’s 100% sure we are dealing with aCircle.
switch statement instead of if/else when dealing with three or more types in a union. It’s cleaner and makes it easier to handle "exhaustiveness checking."
Advantages of Discriminated Unions
- Type Safety: You no longer have to use type assertions (like
as Circle) or manual checks (like"radius" in shape). - Readability: Your code clearly shows the different "states" or "variants" your data can take.
- Flexibility: Adding a new variant, like a
Triangle, is easy. Just add it to the union, and TypeScript will point out exactly where your existing logic needs to be updated.
Example with Complex Types
A real-world use case for this is handling API states. In modern web apps, you usually have three states: Loading, Success, and Error. Discriminated unions make this incredibly easy to manage in a single object.
interface LoadingState {
status: "loading";
}
interface SuccessResponse {
status: "success";
data: string[];
}
interface ErrorResponse {
status: "error";
error: Error;
}
type ApiResponse = LoadingState | SuccessResponse | ErrorResponse;
function renderUI(state: ApiResponse) {
switch (state.status) {
case "loading":
return "Loading spinner...";
case "success":
// Safe to access .data here
return `Items: ${state.data.join(", ")}`;
case "error":
// Safe to access .error here
return `Error: ${state.error.message}`;
}
}
ApiResponse (like "idle") but forget to update your switch statement, your code might fail silently. See the tip below on how to prevent this.
Explanation:
- The
statusproperty acts as the discriminator. - By checking
state.status, we avoid runtime errors where we might try to readstate.datawhile the data is still loading.
Using Discriminated Unions with Functions
Discriminated unions are excellent for user role management. Different users might have different permissions or profile structures.
interface Admin {
role: "admin";
permissions: string[];
}
interface User {
role: "user";
username: string;
}
type Person = Admin | User;
function getDisplayName(person: Person): string {
if (person.role === "admin") {
// TypeScript knows admins have permissions but no username
return `Administrator (Permissions: ${person.permissions.length})`;
} else {
// TypeScript knows users have a username
return `User: ${person.username}`;
}
}
never type. If you assign your variable to a type never in the default case of a switch, TypeScript will throw a compile error if you ever add a new member to the union but forget to handle it.
Summary
Discriminated unions in TypeScript provide a structured, safe way to handle variables that can take on different shapes. By including a literal "tag" property, you give the TypeScript compiler the information it needs to protect your code from common "property undefined" errors.
- Discriminated Union: A union of types linked by a common, literal property (the tag).
- Type Narrowing: The automatic process where TypeScript narrows down the possibilities within a union based on your code's logic.
- Flexibility and Safety: They make your code more predictable and easier to refactor, especially in large-scale applications.
Whether you're handling API responses, Redux actions, or UI component states, discriminated unions are an essential tool in every TypeScript developer's toolkit.