Skip to main content

Advanced Types

TypeScript’s type system is incredibly powerful. This chapter covers advanced type features that enable you to express complex relationships and build type-safe abstractions.

1. Union Types (Revisited)

Union types represent values that can be one of several types.
// Basic union
type StringOrNumber = string | number;

// Discriminated unions (tagged unions)
type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string };

function handleResult<T>(result: Result<T>): void {
  if (result.success) {
    console.log(result.data); // TypeScript knows data exists
  } else {
    console.log(result.error); // TypeScript knows error exists
  }
}

Exhaustive Checks

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number }
  | { kind: 'triangle'; base: number; height: number };

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return (shape.base * shape.height) / 2;
    default:
      // Exhaustive check - will error if a case is missing
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

2. Intersection Types (Revisited)

Intersection types combine multiple types into one.
type Person = {
  name: string;
  age: number;
};

type Employee = {
  employeeId: string;
  department: string;
};

type Staff = Person & Employee;

const staff: Staff = {
  name: 'Alice',
  age: 30,
  employeeId: 'E001',
  department: 'Engineering'
};

Intersection with Functions

type Logger = {
  log: (message: string) => void;
};

type Formatter = {
  format: (data: object) => string;
};

type LogFormatter = Logger & Formatter;

const logFormatter: LogFormatter = {
  log: (message) => console.log(message),
  format: (data) => JSON.stringify(data)
};

3. Conditional Types

Types that depend on conditions.

Basic Syntax

// T extends U ? X : Y
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false
type C = IsString<'hello'>; // true

Practical Examples

// Extract return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function greet(): string {
  return 'Hello';
}

type GreetReturn = ReturnType<typeof greet>; // string

// Flatten arrays
type Flatten<T> = T extends (infer U)[] ? U : T;

type StrArray = Flatten<string[]>; // string
type Num = Flatten<number>;        // number

Distributive Conditional Types

When conditional types act on unions, they distribute over each member.
type ToArray<T> = T extends any ? T[] : never;

type StrOrNumArray = ToArray<string | number>;
// Result: string[] | number[]

// To prevent distribution, wrap in tuple
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;

type Mixed = ToArrayNonDistributive<string | number>;
// Result: (string | number)[]

4. Mapped Types

Transform properties of existing types.

Basic Mapped Types

type User = {
  id: number;
  name: string;
  email: string;
};

// Make all properties optional
type PartialUser = {
  [K in keyof User]?: User[K];
};

// Make all properties readonly
type ReadonlyUser = {
  readonly [K in keyof User]: User[K];
};

// Make all properties nullable
type NullableUser = {
  [K in keyof User]: User[K] | null;
};

Mapped Type Modifiers

// Remove readonly
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

// Remove optional
type Required<T> = {
  [K in keyof T]-?: T[K];
};

// Add readonly and optional
type PartialReadonly<T> = {
  readonly [K in keyof T]?: T[K];
};

Key Remapping (TS 4.1+)

// Rename keys with template literals
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type User = {
  name: string;
  age: number;
};

type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number; }

// Filter out certain keys
type RemoveKind<T> = {
  [K in keyof T as Exclude<K, 'kind'>]: T[K];
};

5. Template Literal Types

Create types from string patterns.
// Basic template literal
type Greeting = `Hello, ${string}`;
const g1: Greeting = 'Hello, World';  // ✅
// const g2: Greeting = 'Hi, World';  // ❌

// Combining unions
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Path = '/users' | '/products';
type Endpoint = `${HttpMethod} ${Path}`;
// 12 combinations: 'GET /users' | 'GET /products' | 'POST /users' | ...

// String manipulations
type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;

type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'

Practical Example: CSS Units

type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CSSValue = `${number}${CSSUnit}`;

const width: CSSValue = '100px';   // ✅
const height: CSSValue = '50vh';   // ✅
// const bad: CSSValue = '100';    // ❌

type CSSProperties = {
  width?: CSSValue;
  height?: CSSValue;
  margin?: CSSValue;
  padding?: CSSValue;
};

6. infer Keyword

Extract types from complex structures.
// Infer return type
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type FnReturn = GetReturnType<() => string>; // string

// Infer array element type
type ArrayElement<T> = T extends (infer E)[] ? E : never;

type El = ArrayElement<number[]>; // number

// Infer Promise resolved type
type Awaited<T> = T extends Promise<infer U> ? U : T;

type Resolved = Awaited<Promise<string>>; // string

// Infer function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

type Params = Parameters<(a: string, b: number) => void>; // [string, number]

Advanced infer Patterns

// Extract first element of tuple
type First<T> = T extends [infer F, ...any[]] ? F : never;

type FirstEl = First<[string, number, boolean]>; // string

// Extract last element
type Last<T> = T extends [...any[], infer L] ? L : never;

type LastEl = Last<[string, number, boolean]>; // boolean

// Infer from string template
type ExtractPath<T> = T extends `/api/${infer Path}` ? Path : never;

type Path = ExtractPath<'/api/users'>; // 'users'

7. Built-in Utility Types

TypeScript provides many utility types out of the box.

Object Utilities

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// Partial<T> - All properties optional
type UpdateUser = Partial<User>;

// Required<T> - All properties required
type CompleteUser = Required<Partial<User>>;

// Readonly<T> - All properties readonly
type FrozenUser = Readonly<User>;

// Pick<T, K> - Select properties
type UserCredentials = Pick<User, 'email' | 'password'>;

// Omit<T, K> - Exclude properties
type PublicUser = Omit<User, 'password'>;

// Record<K, T> - Object with keys K and values T
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;

Union Utilities

type Status = 'pending' | 'active' | 'inactive' | 'deleted';

// Exclude<T, U> - Remove types from union
type ActiveStatus = Exclude<Status, 'deleted'>; // 'pending' | 'active' | 'inactive'

// Extract<T, U> - Keep only matching types
type CommonStatus = Extract<Status, 'active' | 'unknown'>; // 'active'

// NonNullable<T> - Remove null and undefined
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string

Function Utilities

function fetchUser(id: number, options?: { cache: boolean }): Promise<User> {
  // ...
}

// Parameters<T> - Get function parameters as tuple
type FetchParams = Parameters<typeof fetchUser>; // [number, { cache: boolean }?]

// ReturnType<T> - Get function return type
type FetchReturn = ReturnType<typeof fetchUser>; // Promise<User>

// ConstructorParameters<T> - Get constructor parameters
class User {
  constructor(public name: string, public age: number) {}
}
type UserParams = ConstructorParameters<typeof User>; // [string, number]

// InstanceType<T> - Get instance type of constructor
type UserInstance = InstanceType<typeof User>; // User

String Utilities

type Event = 'click' | 'focus' | 'blur';

type UpperEvent = Uppercase<Event>;     // 'CLICK' | 'FOCUS' | 'BLUR'
type LowerEvent = Lowercase<UpperEvent>; // 'click' | 'focus' | 'blur'
type CapEvent = Capitalize<Event>;      // 'Click' | 'Focus' | 'Blur'
type UncapEvent = Uncapitalize<CapEvent>; // 'click' | 'focus' | 'blur'

8. Custom Utility Types

Build your own utility types.

Deep Partial

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

interface Config {
  server: {
    host: string;
    port: number;
  };
  database: {
    url: string;
    name: string;
  };
}

const partialConfig: DeepPartial<Config> = {
  server: { host: 'localhost' }
  // port, database.url, database.name are all optional
};

DeepReadonly

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

const config: DeepReadonly<Config> = {
  server: { host: 'localhost', port: 3000 },
  database: { url: 'mongodb://...', name: 'mydb' }
};

// config.server.host = 'other'; // ❌ Error

Nullable

type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

type User = {
  name: string;
  email: string;
};

type NullableUser = Nullable<User>;
// { name: string | null; email: string | null; }

PickByType

type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

type StringProps = PickByType<User, string>;
// { name: string; email: string; }

type NumberProps = PickByType<User, number>;
// { id: number; age: number; }

OmitByType

type OmitByType<T, U> = {
  [K in keyof T as T[K] extends U ? never : K]: T[K];
};

type NonStringProps = OmitByType<User, string>;
// { id: number; age: number; }

9. Type Challenges

Practice advanced types with these patterns:

Tuple to Union

type TupleToUnion<T extends readonly any[]> = T[number];

type Tuple = [string, number, boolean];
type Union = TupleToUnion<Tuple>; // string | number | boolean

Union to Intersection

type UnionToIntersection<U> = (
  U extends any ? (arg: U) => void : never
) extends (arg: infer I) => void
  ? I
  : never;

type Union = { a: string } | { b: number };
type Intersection = UnionToIntersection<Union>;
// { a: string } & { b: number }

Get Required Keys

type RequiredKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];

type User = {
  id: number;
  name: string;
  email?: string;
};

type Required = RequiredKeys<User>; // 'id' | 'name'

Get Optional Keys

type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];

type Optional = OptionalKeys<User>; // 'email'

10. Type-safe API Patterns

Type-safe Event Emitter

type EventMap = {
  login: { userId: string; timestamp: Date };
  logout: { userId: string };
  purchase: { productId: string; amount: number };
};

class TypedEventEmitter<T extends Record<string, any>> {
  private listeners = new Map<keyof T, Set<(data: any) => void>>();

  on<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(listener);
  }

  emit<K extends keyof T>(event: K, data: T[K]): void {
    this.listeners.get(event)?.forEach((listener) => listener(data));
  }

  off<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
    this.listeners.get(event)?.delete(listener);
  }
}

const emitter = new TypedEventEmitter<EventMap>();

// Fully type-safe!
emitter.on('login', (data) => {
  console.log(data.userId);    // string
  console.log(data.timestamp); // Date
});

emitter.emit('login', { userId: '123', timestamp: new Date() });
// emitter.emit('login', { userId: 123 }); // ❌ Error

Type-safe Builder

type Builder<T, Built extends Partial<T> = {}> = {
  set<K extends keyof T>(
    key: K,
    value: T[K]
  ): Builder<T, Built & Pick<T, K>>;
  build(): Built extends T ? T : never;
};

// This pattern ensures all required properties are set before build()

Summary

ConceptExample
Uniontype A = string | number
Intersectiontype A = B & C
ConditionalT extends U ? X : Y
Mapped{ [K in keyof T]: T[K] }
Template Literaltype A = `on${string}`
inferT extends (infer U)[] ? U : never
PartialPartial<T>
RequiredRequired<T>
PickPick<T, 'a' | 'b'>
OmitOmit<T, 'a' | 'b'>
ReturnTypeReturnType<typeof fn>
ParametersParameters<typeof fn>
Next, we’ll take a deep dive into generics!