Что такое условные и маппированные типы в TypeScript?

SeniorTypeScript #typescript #advanced

Вопрос

Как работают условные типы (conditional types) и маппированные типы (mapped types) и зачем они нужны?

Короткий ответ

Условный тип T extends U ? X : Y выбирает один из двух типов в зависимости от того, совместим ли T с U — это условное ветвление на уровне типов. Маппированный тип { [K in keyof T]: ... } строит новый объектный тип, перебирая ключи существующего типа и применяя к каждому трансформацию.

Подробный ответ

Условные типы позволяют выражать логику "если тип удовлетворяет условию — один результат, иначе другой", вычисляемую компилятором на этапе анализа типов:

type IsString<T> = T extends string ? true : false

type A = IsString<'hello'> // true
type B = IsString<42>      // false

Ключевая возможность условных типов — оператор infer, который позволяет "извлечь" часть типа внутри условия:

type ElementType<T> = T extends (infer U)[] ? U : T

type A = ElementType<string[]> // string
type B = ElementType<number>   // number (T не массив, ветка else)

type ReturnOf<F> = F extends (...args: any[]) => infer R ? R : never
type R1 = ReturnOf<() => string> // string

Когда T — union-тип, условный тип автоматически дистрибутируется: применяется к каждому члену union по отдельности, а результаты объединяются обратно в union:

type ToArray<T> = T extends any ? T[] : never
type Result = ToArray<string | number> // string[] | number[], а не (string | number)[]

Маппированные типы перебирают все ключи keyof T и строят новый тип, применяя модификаторы к каждому свойству. Это основа многих встроенных utility-типов (Partial, Readonly, Pick реализованы именно через mapped types):

type MyPartial<T> = { [K in keyof T]?: T[K] }
type MyReadonly<T> = { readonly [K in keyof T]: T[K] }

С помощью as внутри mapped type можно переименовывать ключи (key remapping) — например, добавлять префикс или фильтровать свойства через условный тип, возвращающий never для исключаемых ключей:

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

interface Person { name: string; age: number }
type PersonGetters = Getters<Person>
// { getName: () => string; getAge: () => number }

Пример

// извлечение типа успешного результата из discriminated union
type ExtractSuccess<T> = T extends { status: 'success'; data: infer D } ? D : never

type ApiResult =
  | { status: 'success'; data: { id: number } }
  | { status: 'error'; message: string }

type SuccessData = ExtractSuccess<ApiResult> // { id: number }

// mapped type: делаем все свойства объекта nullable
type Nullable<T> = { [K in keyof T]: T[K] | null }

interface Form { email: string; age: number }
type NullableForm = Nullable<Form>
// { email: string | null; age: number | null }

// комбинация conditional + mapped: убрать все методы (оставить только данные)
type NonFunctionKeys<T> = {
  [K in keyof T]: T[K] extends (...args: any[]) => any ? never : K
}[keyof T]

type DataOnly<T> = Pick<T, NonFunctionKeys<T>>

class User {
  name = 'Alice'
  age = 30
  greet() { return `Hi, ${this.name}` }
}

type UserData = DataOnly<User> // { name: string; age: number }

Дополнительные вопросы

  • Как работает дистрибутивность условных типов и как её отключить (обернуть T в [T])?
  • Чем infer в условных типах отличается от обычного дженерик-параметра функции?
  • Как реализовать utility-тип DeepPartial<T>, рекурсивно делающий вложенные объекты опциональными?