Skip to content
⚠️ This article was written in 2018. Some content may be outdated.

TypeScript Advanced Types: Conditional Types and Mapped Types

TypeScript 2.8 introduced conditional types, and TypeScript 2.1 introduced mapped types. These two features can be combined to build extremely powerful type utilities.

Mapped Types ​

Generate new types based on an existing type:

typescript
// Make all properties optional
type Partial<T> = {
  [K in keyof T]?: T[K];
};

// Make all properties readonly
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Make all properties required
type Required<T> = {
  [K in keyof T]-?: T[K]; // -? removes the optional modifier
};

// Change all property values to another type
type Record<K extends keyof any, V> = {
  [P in K]: V;
};

// Examples
interface User {
  name: string;
  age: number;
  email?: string;
}

type PartialUser = Partial<User>;
// { name?: string; age?: number; email?: string }

type UserRecord = Record<"admin" | "editor", User>;
// { admin: User; editor: User }

Conditional Types ​

typescript
// T extends U ? X : Y
// If T is assignable to U, the type is X; otherwise Y

type IsArray<T> = T extends any[] ? true : false;

type A = IsArray<string[]>; // true
type B = IsArray<number>; // false

Built-in Utility Types ​

typescript
// Extract: extract from T what can be assigned to U
type Extract<T, U> = T extends U ? T : never;

type NumberOrString = Extract<string | number | boolean, string | number>;
// string | number

// Exclude: exclude from T what can be assigned to U
type Exclude<T, U> = T extends U ? never : T;

type NotBoolean = Exclude<string | number | boolean, boolean>;
// string | number

// ReturnType: get the return type of a function
type ReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any;

function fetchUser(): Promise<{ id: number; name: string }> {
  return fetch("/api/user").then((r) => r.json());
}

type FetchResult = ReturnType<typeof fetchUser>;
// Promise<{ id: number; name: string }>

infer: Type Inference ​

infer is used to "capture" a sub-type inside a conditional type:

typescript
// Get the inner type of a Promise
type Awaited<T> = T extends Promise<infer Inner> ? Inner : T;

type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<number[]>>; // number[]
type C = Awaited<string>; // string (not a Promise — returned as is)

// Get the element type of an array
type ArrayItem<T> = T extends (infer Item)[] ? Item : never;

type N = ArrayItem<number[]>; // number
type S = ArrayItem<string[]>; // string

// Get the type of a function's first parameter
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any
  ? F
  : never;

type P = FirstParam<(a: string, b: number) => void>; // string

Real-world Use: API Type Utilities ​

typescript
// Define the API response structure
interface ApiResponse<T> {
  code: number;
  data: T;
  message: string;
}

// Extract the data type from an API response
type ExtractData<T> = T extends ApiResponse<infer D> ? D : never;

// Define an API function
async function getUserList(): Promise<ApiResponse<User[]>> {
  const res = await fetch("/api/users");
  return res.json();
}

// Infer: UserList = User[]
type UserList = ExtractData<Awaited<ReturnType<typeof getUserList>>>;

Omit: Remove Specific Properties ​

typescript
// Built-in since TS 3.5; manual implementation:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

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

// Remove sensitive fields
type PublicUser = Omit<User, "password" | "createdAt">;
// { id: number; name: string }

Summary ​

  • Mapped types: iterate over [K in keyof T] to generate new types
  • Conditional types: T extends U ? X : Y — choose a type based on a condition
  • infer: infer sub-types within conditional types
  • Built-in utilities: Partial, Required, Readonly, Record, Omit, Extract, Exclude
  • These features let you build type-safe utility functions and reduce repetitive type definitions

MIT Licensed