Skip to content

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&lt;infer U&gt; ? U : T]
        DISTRIBUTIVE[Distributive Pattern<br/>Union types distribute over conditionals]
    end
    
    subgraph "Real-World Examples"
        API_RESPONSE[
            API Response Types:<br/>
            type ApiResult&lt;T&gt; = T extends 'success'<br/>
              ? { data: any, status: 200 }<br/>
              : { error: string, status: 400 }
        ]
        
        ARRAY_ELEMENT[
            Array Element Type:<br/>
            type ElementType&lt;T&gt; = T extends (infer U)[]<br/>
              ? U<br/>
              : T
        ]
        
        FUNCTION_RETURN[
            Function Return Type:<br/>
            type ReturnType&lt;T&gt; = T extends (...args: any[]) =&gt; infer R<br/>
              ? R<br/>
              : any
        ]
    end
    
    subgraph "Advanced Conditional Logic"
        NESTED[
            Nested Conditionals:<br/>
            type Deep&lt;T&gt; = T extends string<br/>
              ? 'string'<br/>
              : T extends number<br/>
                ? 'number'<br/>
                : 'other'
        ]
        
        MAPPED_CONDITIONAL[
            Mapped + Conditional:<br/>
            type Optional&lt;T&gt; = {<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&lt;T, K extends keyof T&gt; = {<br/>
              [P in K]: T[P]<br/>
            }
        ]
        
        OMIT_IMPL[
            Omit Implementation:<br/>
            type MyOmit&lt;T, K extends keyof T&gt; = {<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&lt;T&gt; = T extends Promise&lt;infer U&gt;<br/>
              ? Awaited&lt;U&gt;<br/>
              : T
        ]
        
        FLATTEN_ARRAY[
            Array Flattening:<br/>
            type Flatten&lt;T&gt; = 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:

  • T is the type being checked.
  • U is the condition being applied.
  • X is the resulting type if T matches U.
  • Y is the resulting type if T does not match U.
// @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.

TypeScript JavaScript Type Safety Advanced
Share:

Continue Reading

Advanced TypeScript Design Patterns for Enterprise Applications

Explore advanced TypeScript design patterns and architectural approaches for building scalable enterprise applications. Learn how to implement type-safe patterns like dependency injection, factory methods, decorators, and advanced generics. This comprehensive guide covers practical patterns for building maintainable and robust TypeScript applications.

Read article
TypeScriptJavaScriptType Safety

TypeScript Interfaces vs. Types: Choosing the Right Tool for Your Code

When working with TypeScript, defining data structures, function signatures, and other contracts is key to creating robust, type-safe applications. Interfaces and types are two ways to define these structures, but each has unique strengths and use cases. This guide will help you understand the differences between interfaces and types, when to use each, and how they can improve code readability and maintainability.

Read article
TypeScriptJavaScriptType Safety

TypeScript Utility Types: Simplifying Code with Mapped Types

TypeScript offers a suite of utility types designed to simplify complex type transformations, allowing developers to create new types based on existing ones with minimal effort. Utility types make code more readable, maintainable, and type-safe by reducing repetition and eliminating boilerplate. In this guide, we’ll explore the most commonly used TypeScript utility types like Partial, Pick, Omit, and others, and discuss practical scenarios for applying each.

Read article
TypeScriptJavaScriptType Safety

AI-Assisted Content

This article includes AI-assisted content that has been reviewed for accuracy. Always test code snippets before use.