ДокументацияTypeScriptПродвинутые типы TypeScript
Продвинутый 14 мин чтения

Продвинутые типы TypeScript

Conditional Types, Mapped Types, Template Literal Types, infer и keyof — продвинутые возможности системы типов TypeScript для создания гибких утилит.

typescriptconditional typesmapped typestemplate literalinferkeyof

keyof

keyof извлекает ключи типа как union строковых литералов:

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

type UserKeys = keyof User // 'name' | 'age' | 'email'

Применение — типобезопасный доступ к свойствам:

function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user: User = { name: 'Анна', age: 25, email: 'anna@mail.ru' }

get(user, 'name')  // string
get(user, 'age')   // number
get(user, 'foo')   // Ошибка: 'foo' не keyof User

typeof

typeof в TypeScript (в контексте типов, не runtime) извлекает тип из переменной:

const config = {
  host: 'localhost',
  port: 3000,
  debug: true,
}

type Config = typeof config
// { host: string; port: number; debug: boolean }

Комбинация keyof typeof для получения ключей конкретного объекта:

type ConfigKeys = keyof typeof config // 'host' | 'port' | 'debug'

Conditional Types

Условный тип выбирает один из двух типов на основе условия:

type IsString<T> = T extends string ? 'yes' : 'no'

type A = IsString<string>  // 'yes'
type B = IsString<number>  // 'no'
type C = IsString<'hello'> // 'yes' (literal-строка extends string)

Практический пример — типизация unwrap:

type Unwrap<T> = T extends Promise<infer U> ? U : T

type A = Unwrap<Promise<string>> // string
type B = Unwrap<number>          // number
type C = Unwrap<Promise<Promise<string>>> // Promise<string>

infer

Ключевое слово infer объявляет переменную типа внутри extends:

type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never

type Fn = (x: number) => string
type Result = ReturnTypeOf<Fn> // string

Извлечение типа элемента массива:

type ElementOf<T> = T extends (infer E)[] ? E : never

type Item = ElementOf<string[]> // string
type Num = ElementOf<number[]>  // number

Извлечение типа из Promise:

type Awaited<T> = T extends Promise<infer U>
  ? U extends Promise<infer V>
    ? V
    : U
  : T

type A = Awaited<Promise<string>>         // string
type B = Awaited<Promise<Promise<number>>> // number
type C = Awaited<boolean>                  // boolean

Несколько infer в одном условии:

type FunctionParams<T> = T extends (first: infer F, second: infer S, ...rest: any[]) => any
  ? { first: F; second: S }
  : never

type Params = FunctionParams<(a: string, b: number) => void>
// { first: string; second: number }

Distributive Conditional Types

Когда условный тип применяется к union, он «распределяется» по каждому варианту:

type ToArray<T> = T extends any ? T[] : never

type Result = ToArray<string | number>
// string[] | number[] (не (string | number)[])

Чтобы отключить распределение — оберните в кортеж:

type ToArrayNonDist<T> = [T] extends any ? T[] : never

type Result = ToArrayNonDist<string | number>
// (string | number)[]

Mapped Types

Mapped type создаёт новый тип, преобразуя каждое свойство существующего:

type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

type Optional<T> = {
  [P in keyof T]?: T[P]
}

Синтаксис: [P in keyof T] — перебираем все ключи T, T[P] — берём тип значения.

Изменение типов значений:

type Stringify<T> = {
  [P in keyof T]: string
}

interface User {
  name: string
  age: number
  active: boolean
}

type StringUser = Stringify<User>
// { name: string; age: string; active: string }

Фильтрация ключей через as (TypeScript 4.1+):

type RemoveNull<T> = {
  [P in keyof T as T[P] extends null ? never : P]: T[P]
}

interface Data {
  name: string
  middleName: string | null
  age: number
  nickname: null
}

type WithoutNull = RemoveNull<Data>
// { name: string; age: number }

Преобразование ключей:

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

interface Person {
  name: string
  age: number
}

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

Модификаторы в Mapped Types

+ и - управляют модификаторами readonly и ?:

type Mutable<T> = {
  -readonly [P in keyof T]: T[P]
}

interface Frozen {
  readonly name: string
  readonly age: number
}

type Thawed = Mutable<Frozen>
// { name: string; age: number } — без readonly

Убрать optional:

type Required<T> = {
  [P in keyof T]-?: T[P]
}

Template Literal Types

Шаблонные типы работают как шаблонные строки, но на уровне системы типов:

type EventName = `on${Capitalize<string>}`

type CSSProperty = `margin-${'top' | 'right' | 'bottom' | 'left'}`

type Margin = CSSProperty
// 'margin-top' | 'margin-right' | 'margin-bottom' | 'margin-left'

Встроенные утилиты для работы со строками:

УтилитаОписаниеПример
Uppercase<S>Верхний регистрUppercase<'hello'>'HELLO'
Lowercase<S>Нижний регистрLowercase<'HELLO'>'hello'
Capitalize<S>Первая буква заглавнаяCapitalize<'hello'>'Hello'
Uncapitalize<S>Первая буква строчнаяUncapitalize<'Hello'>'hello'

Практический пример — CSS-свойства:

type Direction = 'top' | 'right' | 'bottom' | 'left'
type CSSMargin = `margin-${Direction}`
type CSSPadding = `padding-${Direction}`

function setMargin(prop: CSSMargin, value: string) {
  // prop: 'margin-top' | 'margin-right' | 'margin-bottom' | 'margin-left'
}

Практические примеры

Типобезопасный Event Bus:

type EventMap = {
  'user:login': { userId: string }
  'user:logout': undefined
  'page:view': { url: string }
}

type EventHandler<T extends keyof EventMap> = EventMap[T] extends undefined
  ? () => void
  : (data: EventMap[T]) => void

function on<T extends keyof EventMap>(event: T, handler: EventHandler<T>) {
  // ...
}

on('user:login', (data) => {
  console.log(data.userId) // типизировано
})

on('user:logout', () => {
  // без параметров
})

Глубокий Readonly:

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

interface Config {
  db: {
    host: string
    port: number
  }
  cache: {
    enabled: boolean
  }
}

type FrozenConfig = DeepReadonly<Config>

Паттерн Builder:

type Builder<T> = {
  [P in keyof T as `with${Capitalize<string & P>}`]: (value: T[P]) => Builder<T>
} & {
  build: () => T
}

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

type UserBuilder = Builder<User>
// {
//   withName: (value: string) => Builder<User>
//   withAge: (value: number) => Builder<User>
//   withEmail: (value: string) => Builder<User>
//   build: () => User
// }

Итог

Продвинутые типы — инструмент для создания сложных, переиспользуемых утилит типа. keyof и typeof — основа, conditional types с infer — мощный механизм извлечения типов, mapped types — для трансформации форм объектов, а template literal types — для генерации строковых типов. Эти возможности используются во встроенных Utility Types и в типобезопасных библиотеках.