TypeScript's type system offers powerful tools for modeling complex data structures with precision and safety. Among the most valuable of these tools are union types, intersection types, and discriminated unions--patterns that enable developers to create robust, self-documenting code that catches errors at compile time rather than runtime. Whether you're handling API responses with varying shapes, building form validation systems, or creating flexible component props in React applications, mastering these type constructs will dramatically improve your code quality and developer experience.
This guide explores these essential TypeScript patterns from fundamentals through advanced techniques, providing practical examples you can apply immediately to your web development projects. Understanding these patterns is essential for building scalable applications with our React development services or when working with our Next.js development team.
Union Types Fundamentals
Understand how union types create 'or' relationships between types and enable flexible type definitions.
Intersection Types
Learn to combine multiple types into one using the '&' operator for rich, composed type definitions.
Discriminated Unions
Master the powerful pattern combining unions with common literal discriminators for exhaustive type checking.
Real-World Patterns
Apply these patterns to API responses, form validation, and component prop systems in your projects.
What Are Union Types?
Union types represent values that can be one of several types. Using the pipe (|) operator, you can express that a variable might be string OR number OR a custom type, and TypeScript will enforce correct handling of each possibility.
According to the TypeScript Handbook, union types are fundamental building blocks for creating flexible type definitions that maintain type safety.
Basic Union Type Syntax
// A value that can be either string or number
type StringOrNumber = string | number;
// Function accepting multiple types
function formatValue(value: string | number): string {
return `Value: ${value}`;
}
Union types are particularly useful in API integration scenarios where responses may have different shapes depending on the outcome. When combined with proper error handling, these types create robust systems that handle both expected variations and edge cases gracefully.
Type Narrowing with Union Types
When working with union types, TypeScript's control flow analysis narrows the specific type within conditional branches, enabling safe type-specific operations.
function processInput(input: string | number) {
if (typeof input === 'string') {
// TypeScript knows 'input' is a string here
return input.toUpperCase();
}
// TypeScript knows 'input' is a number here
return input.toFixed(2);
}
TypeScript automatically narrows the type based on runtime checks, allowing you to safely access type-specific methods without additional casting. This pattern is especially valuable when building form validation systems where different validation rules apply based on input type. The combination of union types and type narrowing creates a powerful foundation for creating type-safe validation logic that catches errors at compile time.
Understanding Intersection Types
Intersection types use the ampersand (&) operator to combine multiple types into a single type that includes all properties from each constituent type. This pattern is essential for composing rich, layered type definitions.
As demonstrated in practical examples from the LogRocket Blog, intersection types excel at combining separate concerns into unified type structures.
Basic Intersection Type Syntax
interface ContactInfo {
email: string;
phone?: string;
}
interface PersonalDetails {
name: string;
age: number;
}
// Combined type with all properties
type Employee = ContactInfo & PersonalDetails;
const employee: Employee = {
name: "Sarah",
email: "[email protected]",
age: 32
// phone is optional
};
When combining object types with intersections, TypeScript merges properties, with required properties taking precedence over optional ones. This approach is invaluable when composing complex enterprise application types. For teams building large-scale systems, intersection types help maintain type safety while allowing flexible composition of domain models.
Discriminated Unions: The Power Pattern
Discriminated unions (also called tagged unions or algebraic data types) combine union types with a shared literal property that serves as a "discriminator." This pattern enables exhaustive type checking and is fundamental to handling complex state machines and API responses. Our full-stack development team regularly uses this pattern for state management in production applications.
Anatomy of a Discriminated Union
// Step 1: Define each variant with a discriminator
interface Success {
kind: 'success';
data: unknown;
timestamp: number;
}
interface Error {
kind: 'error';
message: string;
code: number;
}
interface Loading {
kind: 'loading';
progress: number;
}
// Step 2: Create the union type
type ApiResponse = Success | Error | Loading;
The common kind property acts as a discriminator, allowing TypeScript to narrow to the specific variant when you check its value in a switch statement or conditional chain. This pattern pairs excellently with GraphQL implementations where you need to handle different response types from your queries.
Exhaustive Type Checking
The never type in the default case creates a compile-time guarantee: if you add a new variant to the union but forget to handle it in the switch statement, TypeScript will raise an error.
function handleResponse(response: ApiResponse) {
switch (response.kind) {
case 'success':
// TypeScript knows response is Success
console.log('Data:', response.data);
break;
case 'error':
// TypeScript knows response is Error
console.error('Error:', response.message);
break;
case 'loading':
// TypeScript knows response is Loading
console.log('Loading:', response.progress);
break;
default:
// Exhaustiveness check - compile error if case is missing
const _exhaustive: never = response;
throw new Error(`Unknown response type: ${_exhaustive}`);
}
}
This makes discriminated unions invaluable for Redux-style state management, API response handling, form state management, and multi-step workflows. When building custom web applications, this pattern prevents entire categories of runtime bugs before they can occur. Combined with React hooks for state management, you create unbreakable state machines.
Real-World Web Development Patterns
Pattern 1: API Response Handling
interface ApiSuccess<T> {
status: 'success';
data: T;
meta?: {
page: number;
perPage: number;
};
}
interface ApiError {
status: 'error';
error: {
code: string;
message: string;
};
}
interface ApiLoading {
status: 'loading';
pendingTime: number;
}
type ApiResult<T> = ApiSuccess<T> | ApiError | ApiLoading;
Pattern 2: Form Validation State
interface FormFieldValid {
state: 'valid';
value: string;
}
interface FormFieldInvalid {
state: 'invalid';
value: string;
errors: string[];
}
interface FormFieldPending {
state: 'pending';
value: string;
}
type FormFieldState = FormFieldValid | FormFieldInvalid | FormFieldPending;
Pattern 3: Component Props with Variants
interface ButtonPrimary {
variant: 'primary';
onClick: () => void;
}
interface ButtonSecondary {
variant: 'secondary';
onClick: () => void;
}
interface ButtonDestructive {
variant: 'destructive';
confirmText: string;
onConfirm: () => void;
}
type ButtonProps = ButtonPrimary | ButtonSecondary | ButtonDestructive;
These patterns are core to our React development services and Next.js solutions, ensuring type safety throughout the application lifecycle. For developers looking to deepen their understanding of these patterns, our guide on advanced Next.js caching strategies demonstrates how discriminated unions work with server-side caching patterns.
Best Practices
-
Always use discriminated unions over plain unions for complex states - The compile-time exhaustiveness checking is invaluable.
-
Keep discriminators as literal types - Use
'success' | 'error' | 'loading'rather thanstringfor better type inference. -
Prefer interfaces over type aliases for extendable types - Interfaces can be extended later; types are more rigid.
-
Use the
nevertype for exhaustive checks - This catches missing cases at compile time. -
Consider creating type guard functions - Use
isassertion for user-defined type guards. -
Document discriminator meanings - Other developers need to understand what each variant represents.
Common Pitfalls to Avoid
- Forgetting to handle all union members - Always use exhaustive switch statements or the
neverpattern. - Using overly broad discriminators - Avoid
stringornumberas discriminators; use literal types. - Creating ambiguous unions - Ensure each variant is clearly distinguishable.
- Overusing intersection types - Combining too many types can lead to complex, hard-to-debug types.
By following these guidelines in your custom web application development, you'll create maintainable type systems that scale with your project. These patterns work hand-in-hand with custom React hooks to create type-safe, composable application logic.
Performance Considerations
TypeScript's type system operates entirely at compile time, so union and intersection types do not directly impact runtime performance. However, well-designed types can improve performance by:
- Enabling better tree-shaking in bundlers - Precise types help eliminate unused code
- Providing clearer error messages during development - Faster debugging and development cycles
- Reducing runtime type assertions needed - Less defensive coding at runtime
- Improving IDE performance through better type inference - Faster autocomplete and navigation
The type safety you gain from these patterns is purely additive to your development experience and has no runtime cost. This is one reason why enterprise web development projects benefit significantly from TypeScript's advanced type system.
When working with our API development services, proper type definitions also enable better documentation and faster integration for consuming applications. For teams deploying to production, understanding these patterns complements our guide on deploying Next.js applications for complete type safety from development through deployment.
Frequently Asked Questions
Sources
- TypeScript Handbook - Unions and Intersection Types - Official TypeScript documentation covering union and intersection type fundamentals
- LogRocket Blog - TypeScript Discriminated Union and Intersection Types - Practical guide with real-world examples