Что такое условные и маппированные типы в TypeScript?
Вопрос
Как работают условные типы (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>, рекурсивно делающий вложенные объекты опциональными?