TypeScript Advanced Types: Mastering Union, Intersection, and Conditional Types
TypeScript Advanced Types: Mastering Union, Intersection, and Conditional Types
As you progress in TypeScript, understanding advanced types such as union types, intersection types, and conditional types can enhance the flexibility, readability, and type safety of your code. These types allow for precise type definitions, dynamic type combinations, and even conditional logic within types, making them invaluable tools in TypeScript development. In this guide, we’ll dive into these advanced types with practical examples and explore how they can optimize your TypeScript code.
TypeScript Advanced Types Hierarchy
graph TB
subgraph "Basic Types Foundation"
PRIMITIVE[Primitive Types<br/>string, number, boolean]
OBJECT[Object Types<br/>interface, type aliases]
ARRAY[Array Types<br/>T[], Array<T>]
FUNCTION[Function Types<br/>(args) => ReturnType]
end
subgraph "Advanced Type Compositions"
UNION[Union Types<br/>string | number | boolean]
INTERSECTION[Intersection Types<br/>Type1 & Type2 & Type3]
CONDITIONAL[Conditional Types<br/>T extends U ? X : Y]
MAPPED[Mapped Types<br/>{[K in keyof T]: U}]
end
subgraph "Utility Type Applications"
PARTIAL[Partial<T><br/>All properties optional]
REQUIRED[Required<T><br/>All properties required]
PICK[Pick<T, K><br/>Select specific properties]
OMIT[Omit<T, K><br/>Exclude specific properties]
RECORD[Record<K, T><br/>Key-value mapping]
end
subgraph "Template Literal Types"
TEMPLATE[Template Literals<br/>\`prefix-\${string}\`]
UPPERCASE[Uppercase<T><br/>Convert to uppercase]
LOWERCASE[Lowercase<T><br/>Convert to lowercase]
CAPITALIZE[Capitalize<T><br/>Capitalize first letter]
end
subgraph "Advanced Patterns"
GENERIC[Generic Constraints<br/>T extends KeyType]
INFER[Infer Types<br/>infer keyword usage]
RECURSIVE[Recursive Types<br/>Self-referencing definitions]
BRAND[Branded Types<br/>Nominal typing simulation]
end
PRIMITIVE --> UNION
OBJECT --> INTERSECTION
FUNCTION --> CONDITIONAL
ARRAY --> MAPPED
UNION --> PARTIAL
INTERSECTION --> REQUIRED
CONDITIONAL --> PICK
MAPPED --> OMIT
CONDITIONAL --> TEMPLATE
MAPPED --> UPPERCASE
UNION --> LOWERCASE
INTERSECTION --> CAPITALIZE
CONDITIONAL --> INFER
MAPPED --> RECURSIVE
UNION --> GENERIC
INTERSECTION --> BRAND
style UNION fill:#e8f5e8
style INTERSECTION fill:#e1f5fe
style CONDITIONAL fill:#fff3e0
style MAPPED fill:#f3e5f5
style TEMPLATE fill:#ffebee
Union Types in TypeScript
A union type allows a variable to hold multiple types, making it useful when a value could be one of several specified types. Unions are created using the | (pipe) symbol.
Defining a Union Type
Here’s an example of a union type for a variable that could be either a string or number:
// @filename: index.ts
function printId(id: string | number): void {
console.log('ID:', id)
}
printId(101) // Output: "ID: 101"
printId('ABC123') // Output: "ID: ABC123"
With a union type, the printId function accepts either a string or number. This flexibility can be useful for handling data with varying types, such as IDs or input values.
Using Type Guards with Unions
Type guards allow you to safely access properties or methods by narrowing down the type within a union.
// @filename: index.ts
function display(value: string | number) {
if (typeof value === 'string') {
console.log('String:', value.toUpperCase())
} else {
console.log('Number:', value.toFixed(2))
}
}
display('hello') // Output: "String: HELLO"
display(3.14) // Output: "Number: 3.14"
The typeof operator checks the type at runtime, letting you safely handle each specific type within the union.
Union Types with Custom Types
Union types can combine custom types, providing flexibility for more complex data structures.
// @filename: index.ts
type Dog = { breed: string; bark: () => void }
type Cat = { breed: string; meow: () => void }
function makeSound(animal: Dog | Cat) {
if ('bark' in animal) {
animal.bark()
} else {
animal.meow()
}
}
In this example, makeSound checks for the bark method to differentiate between Dog and Cat types.
Union vs Intersection Types Comparison
graph TB
subgraph "Union Types (A | B)"
UNION_DEF[Type Definition<br/>string | number | boolean]
UNION_LOGIC[Logical OR<br/>Value can be ANY of the types]
UNION_PROPS[Available Properties<br/>Only COMMON properties accessible]
UNION_EXAMPLE[
Example:<br/>
type ID = string | number<br/>
let userId: ID = "abc123"<br/>
let postId: ID = 12345
]
end
subgraph "Intersection Types (A & B)"
INTER_DEF[Type Definition<br/>Employee & Manager & Developer]
INTER_LOGIC[Logical AND<br/>Value must have ALL types' properties]
INTER_PROPS[Required Properties<br/>ALL properties from EACH type]
INTER_EXAMPLE[
Example:<br/>
type FullEmployee = Employee & Manager<br/>
let lead: FullEmployee = {<br/>
id: 1, name: "John",<br/>
department: "Engineering"<br/>
}
]
end
subgraph "Type Narrowing Strategies"
UNION_NARROW[Union Type Guards<br/>typeof, instanceof, "in" operator]
INTER_NARROW[No Narrowing Needed<br/>All properties always available]
DISCRIMINATED[Discriminated Unions<br/>Common property for type identification]
COMPOSITION[Type Composition<br/>Building complex objects]
end
subgraph "Use Cases"
UNION_USES[
Union Use Cases:<br/>
• Function parameters<br/>
• API response types<br/>
• Configuration options<br/>
• Form field values
]
INTER_USES[
Intersection Use Cases:<br/>
• Mixing object behaviors<br/>
• Plugin architectures<br/>
• Role-based permissions<br/>
• Decorator patterns
]
end
subgraph "Code Examples"
UNION_CODE[
function process(value: string | number) {<br/>
if (typeof value === 'string') {<br/>
return value.toUpperCase()<br/>
}<br/>
return value * 2<br/>
}
]
INTER_CODE[
type Trackable = { id: string }<br/>
type Timestamped = { createdAt: Date }<br/>
type User = Trackable & Timestamped & {<br/>
name: string<br/>
}
]
end
UNION_DEF --> UNION_LOGIC
UNION_LOGIC --> UNION_PROPS
UNION_PROPS --> UNION_EXAMPLE
INTER_DEF --> INTER_LOGIC
INTER_LOGIC --> INTER_PROPS
INTER_PROPS --> INTER_EXAMPLE
UNION_PROPS --> UNION_NARROW
INTER_PROPS --> INTER_NARROW
UNION_NARROW --> DISCRIMINATED
INTER_NARROW --> COMPOSITION
UNION_EXAMPLE --> UNION_USES
INTER_EXAMPLE --> INTER_USES
UNION_USES --> UNION_CODE
INTER_USES --> INTER_CODE
style UNION_LOGIC fill:#e8f5e8
style INTER_LOGIC fill:#e1f5fe
style UNION_NARROW fill:#fff3e0
style COMPOSITION fill:#f3e5f5
Intersection Types in TypeScript
An intersection type combines multiple types into one, making it possible for an object to have all properties and methods from each type in the intersection. This is useful for creating composite types, where you need an object with characteristics from multiple types.
Defining an Intersection Type
Intersection types use the & symbol to combine types.
// @filename: index.ts
type Employee = { employeeId: number; name: string }
type Manager = { department: string }
type ManagerEmployee = Employee & Manager
const manager: ManagerEmployee = {
employeeId: 123,
name: 'Alice',
department: 'HR',
}
console.log(manager)
The ManagerEmployee type requires properties from both Employee and Manager, making it suitable for representing roles with overlapping characteristics.
Handling Conflicts in Intersection Types
If two types in an intersection share a property with incompatible types, TypeScript will throw an error. To avoid this, ensure shared properties have compatible types.
// @filename: index.ts
type Car = { name: string; wheels: number }
type Boat = { name: string; capacity: number }
type AmphibiousVehicle = Car & Boat
// Correct Usage
const vehicle: AmphibiousVehicle = {
name: 'Amphicar',
wheels: 4,
capacity: 2,
}
console.log(vehicle)
In this case, AmphibiousVehicle combines the properties of Car and Boat, making it suitable for a vehicle that can operate on both land and water.
Conditional Types in TypeScript
Conditional types allow types to be defined based on conditions, similar to an if-else statement. This provides powerful dynamic typing capabilities, enabling you to create types that depend on other types.
Conditional Types Flow and Patterns
graph TB
subgraph "Conditional Type Syntax"
SYNTAX[T extends U ? X : Y]
CHECK[Type Check<br/>Does T extend U?]
TRUE[✅ True Branch<br/>Return Type X]
FALSE[❌ False Branch<br/>Return Type Y]
end
subgraph "Common Patterns"
EXTRACT[Extract Pattern<br/>T extends string ? T : never]
EXCLUDE[Exclude Pattern<br/>T extends U ? never : T]
INFER[Infer Pattern<br/>T extends Promise<infer U> ? U : T]
DISTRIBUTIVE[Distributive Pattern<br/>Union types distribute over conditionals]
end
subgraph "Real-World Examples"
API_RESPONSE[
API Response Types:<br/>
type ApiResult<T> = T extends 'success'<br/>
? { data: any, status: 200 }<br/>
: { error: string, status: 400 }
]
ARRAY_ELEMENT[
Array Element Type:<br/>
type ElementType<T> = T extends (infer U)[]<br/>
? U<br/>
: T
]
FUNCTION_RETURN[
Function Return Type:<br/>
type ReturnType<T> = T extends (...args: any[]) => infer R<br/>
? R<br/>
: any
]
end
subgraph "Advanced Conditional Logic"
NESTED[
Nested Conditionals:<br/>
type Deep<T> = T extends string<br/>
? 'string'<br/>
: T extends number<br/>
? 'number'<br/>
: 'other'
]
MAPPED_CONDITIONAL[
Mapped + Conditional:<br/>
type Optional<T> = {<br/>
[K in keyof T]: T[K] extends Function<br/>
? T[K]<br/>
: T[K] | undefined<br/>
}
]
end
subgraph "Utility Type Implementations"
PICK_IMPL[
Pick Implementation:<br/>
type MyPick<T, K extends keyof T> = {<br/>
[P in K]: T[P]<br/>
}
]
OMIT_IMPL[
Omit Implementation:<br/>
type MyOmit<T, K extends keyof T> = {<br/>
[P in keyof T as P extends K ? never : P]: T[P]<br/>
}
]
end
subgraph "Type Inference Examples"
PROMISE_UNWRAP[
Promise Unwrapping:<br/>
type Awaited<T> = T extends Promise<infer U><br/>
? Awaited<U><br/>
: T
]
FLATTEN_ARRAY[
Array Flattening:<br/>
type Flatten<T> = T extends (infer U)[]<br/>
? U<br/>
: T
]
end
SYNTAX --> CHECK
CHECK -->|Yes| TRUE
CHECK -->|No| FALSE
TRUE --> EXTRACT
FALSE --> EXCLUDE
CHECK --> INFER
EXTRACT --> API_RESPONSE
INFER --> ARRAY_ELEMENT
EXCLUDE --> FUNCTION_RETURN
API_RESPONSE --> NESTED
ARRAY_ELEMENT --> MAPPED_CONDITIONAL
NESTED --> PICK_IMPL
MAPPED_CONDITIONAL --> OMIT_IMPL
PICK_IMPL --> PROMISE_UNWRAP
OMIT_IMPL --> FLATTEN_ARRAY
style CHECK fill:#e8f5e8
style TRUE fill:#e1f5fe
style FALSE fill:#ffebee
style INFER fill:#fff3e0
style DISTRIBUTIVE fill:#f3e5f5
Basic Conditional Type Syntax
A conditional type has the syntax T extends U ? X : Y, where:
Tis the type being checked.Uis the condition being applied.Xis the resulting type ifTmatchesU.Yis the resulting type ifTdoes not matchU.
// @filename: index.ts
type IsString<T> = T extends string ? 'It’s a string' : 'Not a string'
type Test1 = IsString<string> // "It’s a string"
type Test2 = IsString<number> // "Not a string"
Here, IsString<string> evaluates to "It’s a string", while IsString<number> evaluates to "Not a string".
Extracting Properties with Conditional Types
Conditional types are especially useful for working with specific properties of complex types.
// @filename: index.ts
type ExtractStringProperties<T> = {
[K in keyof T]: T[K] extends string ? K : never
}[keyof T]
interface User {
name: string
age: number
email: string
}
type StringProps = ExtractStringProperties<User> // "name" | "email"
ExtractStringProperties creates a type with only the properties of User that are string, allowing selective access to specific properties.
Conditional Types with Generic Types
Conditional types work well with generic types, making them dynamic based on input types.
// @filename: index.ts
type ArrayElementType<T> = T extends (infer U)[] ? U : T
type StringArray = ArrayElementType<string[]> // string
type NumberArray = ArrayElementType<number[]> // number
type NonArray = ArrayElementType<boolean> // boolean
In this example, ArrayElementType extracts the type of elements in an array or leaves the type unchanged if it’s not an array.
Practical Examples of Advanced Types
Advanced types can be combined and used in real-world scenarios for type safety and dynamic flexibility.
1. Handling API Response Types with Unions and Conditionals
// @filename: index.ts
type SuccessResponse = { status: 'success'; data: string }
type ErrorResponse = { status: 'error'; error: string }
type ApiResponse<T> = T extends 'success' ? SuccessResponse : ErrorResponse
function fetchApi<T extends 'success' | 'error'>(status: T): ApiResponse<T> {
if (status === 'success') {
return { status: 'success', data: 'Data loaded' } as ApiResponse<T>
} else {
return { status: 'error', error: 'Failed to load data' } as ApiResponse<T>
}
}
const successResponse = fetchApi('success')
const errorResponse = fetchApi('error')
In this example, fetchApi returns different types based on the status passed in, leveraging conditional types to create a type-safe API response handler.
2. Combining Types in Form Validation
// @filename: index.ts
type RequiredField = { required: boolean }
type EmailField = { format: 'email' }
type TextField = { minLength: number; maxLength: number }
type FormField = RequiredField & (EmailField | TextField)
const emailField: FormField = {
required: true,
format: 'email',
}
const textField: FormField = {
required: true,
minLength: 5,
maxLength: 50,
}
Here, FormField uses intersections and unions to represent fields that could be either email or text, but must include the required property, creating flexible validation rules.
3. Creating a Function Overload with Conditional Types
Conditional types can be used to create type-safe overloads for functions.
// @filename: index.ts
type Overload<T> = T extends string
? string
: T extends number
? number
: boolean
function parseInput<T extends string | number>(input: T): Overload<T> {
if (typeof input === 'string') {
return input.toUpperCase() as Overload<T>
} else if (typeof input === 'number') {
return (input * 2) as Overload<T>
}
return false as Overload<T>
}
console.log(parseInput('hello')) // Output: "HELLO"
console.log(parseInput(5)) // Output: 10
This function uses a conditional type to determine its return type based on the type of input, creating a flexible overload pattern.
Conclusion
TypeScript’s advanced types, including union, intersection, and conditional types, allow you to define complex and dynamic type structures that enhance the type safety and flexibility of your code. By mastering these types, you can write more adaptable TypeScript code that accurately represents data models, enhances code readability, and minimizes runtime errors.
Use these advanced types in your TypeScript projects to unlock new levels of type precision, creating robust applications that are both maintainable and scalable.
