Skip to content

Instantly share code, notes, and snippets.

@olegopro
Last active June 25, 2025 16:52
Show Gist options
  • Save olegopro/a51ca3fd2de4cc2596abdabd491dc77a to your computer and use it in GitHub Desktop.
Save olegopro/a51ca3fd2de4cc2596abdabd491dc77a to your computer and use it in GitHub Desktop.

Итераторы типов в TypeScript

В TypeScript существуют конструкции, которые работают аналогично итераторам в обычных языках программирования, но оперируют с типами во время компиляции. Эти "итераторы типов" позволяют трансформировать типы и создавать мощные абстракции на уровне типовой системы.

Содержание

Основные "итераторы" в TypeScript

Оператор in в сопоставленных типах

Оператор in в сопоставленных типах (mapped types) итерирует по всем ключам указанного типа, позволяя создавать новые типы на основе существующих.

Синтаксис:

type TransformedType<T> = { 
  [K in keyof T]: TransformedValueType 
};

Как понимать запись:

  1. keyof T - получает объединение всех ключей типа T
  2. K in - итерация по каждому ключу из этого объединения
  3. [K in keyof T]: ... - для каждого ключа K создаётся свойство с тем же именем
  4. : TransformedValueType - каждому свойству присваивается новый тип

Пример:

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

// Создаем тип, где все поля могут быть null
type NullableUser = { [K in keyof User]: User[K] | null }

// Результат:
// {
//   id: number | null
//   name: string | null
//   email: string | null
// }

Визуализация обработки:

User                          → { id: number, name: string, email: string }
↓ keyof                       → "id" | "name" | "email"  
↓ [K in ...]                  → Итерация по каждому ключу
↓ [K in ...]: User[K] | null  → Создание нового типа с измененными значениями
NullableUser                  → { id: number | null, name: string | null, email: string | null }

Условный тип с extends

Условные типы с extends могут работать как итераторы при использовании с объединениями (union types).

Синтаксис:

type ConditionalType<T> = T extends Condition ? TrueType : FalseType

Как понимать запись:

  1. T extends Condition - проверка: соответствует ли тип T условию Condition
  2. ? TrueType : FalseType - тернарный оператор: если да, то TrueType, иначе FalseType
  3. Когда T - объединение типов, условие применяется к каждому элементу объединения по отдельности

Пример:

type Colors = "red" | "green" | "blue" | "yellow"

// Фильтрация только основных цветов
type PrimaryColors<T> = T extends "red" | "blue" | "green" ? T : never

// Результат: "red" | "blue" | "green"
type Primary = PrimaryColors<Colors>

Визуализация обработки:

Colors               → "red" | "green" | "blue" | "yellow"
↓ T extends ...      → Итерация по каждому элементу объединения

↓ Проверка каждого элемента:
  "red" extends "red" | "blue" | "green" ? "red" : never        → "red"
  "green" extends "red" | "blue" | "green" ? "green" : never    → "green"
  "blue" extends "red" | "blue" | "green" ? "blue" : never      → "blue"
  "yellow" extends "red" | "blue" | "green" ? "yellow" : never  → never

↓ Объединение результатов
Primary              → "red" | "green" | "blue"

Оператор infer

Оператор infer используется в условных типах для извлечения частей других типов. Хотя сам по себе не является итератором, он часто применяется с дистрибутивными условными типами.

Синтаксис:

type InferredType<T> = T extends Pattern<infer R> ? R : DefaultType;

Как понимать запись:

  1. infer R - объявляет переменную типа R, которая будет выведена из структуры T
  2. T extends Pattern<infer R> - проверяет, соответствует ли T шаблону и извлекает часть типа
  3. ? R : DefaultType - если соответствует, возвращает извлеченный тип R, иначе DefaultType

Пример:

// Извлечение типа элементов массива
type ArrayElementType<T> = T extends Array<infer E> ? E : never

type Numbers = ArrayElementType<number[]> // number
type Strings = ArrayElementType<string[]> // string
type Mixed = ArrayElementType<(string | number)[]> // string | number

Визуализация обработки:

T = number[]
↓ Проверка T extends Array<infer E>   → true, E = number
↓ Результат: E                        → number

T = string[]
↓ Проверка T extends Array<infer E>   → true, E = string
↓ Результат: E                        → string

T = (string | number)[]
↓ Проверка T extends Array<infer E>   → true, E = string | number
↓ Результат: E                        → string | number

Рекурсивные типы

Рекурсивные типы позволяют итерировать по вложенным структурам данных, обрабатывая каждый уровень вложенности.

Синтаксис:

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

Как понимать запись:

  1. [K in keyof T] - итерация по всем ключам T
  2. T[K] extends object ? ... : ... - проверка: является ли значение объектом
  3. Если значение - объект, рекурсивно применяем тот же тип к нему
  4. Если не объект, применяем другую трансформацию

Пример:

// Глубокий ReadOnly тип
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]
};

type User = {
  id: number
  name: string
  settings: {
    theme: string
    notifications: {
      email: boolean
      push: boolean
    }
  }
};

// Теперь все поля на всех уровнях вложенности только для чтения
type ReadonlyUser = DeepReadonly<User>

Визуализация обработки:

User
↓ [K in keyof T] итерирует по "id", "name", "settings"
↓ Для "id": T[K] extends object? → false → readonly id: number
↓ Для "name": T[K] extends object? → false → readonly name: string
↓ Для "settings": T[K] extends object? → true → рекурсивный вызов DeepReadonly<settings>
  ↓ [K in keyof settings] итерирует по "theme", "notifications"
  ↓ Для "theme": settings[K] extends object? → false → readonly theme: string
  ↓ Для "notifications": settings[K] extends object? → true → рекурсивный вызов DeepReadonly<notifications>
    ↓ [K in keyof notifications] итерирует по "email", "push"
    ↓ Для "email": notifications[K] extends object? → false → readonly email: boolean
    ↓ Для "push": notifications[K] extends object? → false → readonly push: boolean
  ↓ Результат: readonly notifications: { readonly email: boolean; readonly push: boolean }
↓ Результат: readonly settings: { readonly theme: string; readonly notifications: ... }

Шаблонные литеральные типы

Шаблонные литеральные типы (template literal types) позволяют итерировать по элементам объединения в строковых шаблонах.

Синтаксис:

type TemplateType<T extends string> = `prefix-${T}-suffix`

Как понимать запись:

  1. T extends string - ограничивает T строковым типом
  2. `prefix-${T}-suffix` - создает шаблон строки с подстановкой T
  3. Если T - объединение строк, шаблон автоматически применяется к каждому элементу

Пример:

type Sizes = "small" | "medium" | "large"
type CssClasses = `size-${Sizes}`

// Результат: "size-small" | "size-medium" | "size-large"

// Более сложный пример с несколькими объединениями
type Colors = "red" | "blue"
type Positions = "top" | "bottom"
type ColorPositions = `${Colors}-${Positions}`

// Результат: "red-top" | "red-bottom" | "blue-top" | "blue-bottom"

Визуализация обработки:

Sizes = "small" | "medium" | "large"
↓ `size-${Sizes}` итерирует по каждому элементу Sizes
↓ "size-" + "small"  → "size-small"
↓ "size-" + "medium" → "size-medium"
↓ "size-" + "large"  → "size-large"
↓ Объединение результатов
CssClasses → "size-small" | "size-medium" | "size-large"

Дистрибутивность условных типов

Дистрибутивность - это одна из ключевых особенностей условных типов в TypeScript, которая позволяет им автоматически распространяться на элементы объединений.

Как работает дистрибутивность

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

Правило дистрибутивности:

(A | B | C) extends X ? Y : Z  ===  (A extends X ? Y : Z) | (B extends X ? Y : Z) | (C extends X ? Y : Z)

Примеры:

// Стандартный пример дистрибутивности
type ToArray<T> = T extends any ? T[] : never

type NumberOrString = number | string
type Result = ToArray<NumberOrString>
// Результат: number[] | string[]
// НЕ (number | string)[]

Визуализация дистрибутивности:

ToArray<number | string>
↓ Дистрибутивность
↓ (number extends any ? number[] : never) | (string extends any ? string[] : never)
↓ number[] | string[]

Предотвращение дистрибутивности

Иногда дистрибутивность нежелательна. Её можно предотвратить, обернув параметр типа в кортеж:

// Без дистрибутивности
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never

type Result1 = ToArray<NumberOrString>;             // number[] | string[]
type Result2 = ToArrayNonDistributive<NumberOrString>; // (number | string)[]

Визуализация предотвращения дистрибутивности:

ToArrayNonDistributive<number | string>
↓ [number | string] extends [any]?
↓ true → (number | string)[]

Как понимать поток данных в "итераторах" типов

Чтобы лучше понять, как работают "итераторы" в TypeScript, полезно представлять их как направленный поток обработки данных (типов).

Модель обработки типов

Представьте типовые операции как конвейер, где каждая операция трансформирует тип:

Исходный тип → Операция 1 → Промежуточный тип → Операция 2 → ... → Итоговый тип

Пример потока данных для mapped type:

type User = { id: number; name: string; email: string }
type ReadonlyUser = { readonly [K in keyof User]: User[K] }

Поток обработки:

User                            → { id: number; name: string; email: string }
↓ keyof User                    → "id" | "name" | "email"
↓ [K in ...]                    → Итерация по "id", "name", "email"
↓ readonly [K in ...]: User[K]  → Добавление readonly к каждому свойству
ReadonlyUser                    → { readonly id: number; readonly name: string; readonly email: string }

Визуализация потока данных

Мы можем визуализировать работу различных "итераторов" типов:

1. Сопоставленные типы (с оператором in):

Тип T → keyof T → Итерация по каждому ключу → Применение трансформации → Новый тип

2. Условные типы с дистрибутивностью:

Объединение A | B | C → Разбивка на отдельные типы A, B, C → Проверка каждого типа против условия → Сборка результатов в новое объединение

3. Рекурсивные типы:

Тип T → Итерация по свойствам T → Для каждого свойства-объекта: рекурсивный вызов → Для примитивных свойств: прямая трансформация → Сборка результатов

Практические примеры использования

Пример 1: Создание типа с опциональными полями

type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>

interface User {
  id: number
  name: string
  email: string
  phone: string
}

// Сделаем email и phone опциональными
type OptionalContactUser = MakeOptional<User, 'email' | 'phone'>

// Результат:
// {
//   id: number
//   name: string
//   email?: string
//   phone?: string
// }

Пример 2: Извлечение вложенных типов

type NestedPaths<T, Delimiter extends string = '.'> = {
  [K in keyof T]: T[K] extends object
    ? | `${K & string}`
      | `${K & string}${Delimiter}${NestedPaths<T[K], Delimiter> & string}`
    : `${K & string}`
}[keyof T]

type User = {
  id: number
  name: string
  settings: {
    theme: string
    notifications: {
      email: boolean
      sms: boolean
    }
  }
};

type UserPaths = NestedPaths<User>;
// "id" | "name" | "settings" | "settings.theme" | "settings.notifications" |
// "settings.notifications.email" | "settings.notifications.sms"

Пример 3: Тип валидатора объекта

type Validator<T> = {
  [K in keyof T]: (value: T[K]) => boolean
}

interface UserForm {
  username: string
  age: number
  email: string
}

const userValidator: Validator<UserForm> = {
  username: (value) => value.length >= 3,
  age: (value) => value >= 18 && value < 100,
  email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
}

Эти примеры демонстрируют, как итераторы типов позволяют создавать мощные и гибкие абстракции на уровне типовой системы TypeScript.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment