В TypeScript существуют конструкции, которые работают аналогично итераторам в обычных языках программирования, но оперируют с типами во время компиляции. Эти "итераторы типов" позволяют трансформировать типы и создавать мощные абстракции на уровне типовой системы.
- Основные "итераторы" в TypeScript
- Дистрибутивность условных типов
- Как понимать поток данных в "итераторах" типов
- Практические примеры использования
Оператор in
в сопоставленных типах (mapped types) итерирует по всем ключам указанного типа, позволяя создавать новые типы на основе существующих.
Синтаксис:
type TransformedType<T> = {
[K in keyof T]: TransformedValueType
};
Как понимать запись:
keyof T
- получает объединение всех ключей типа TK in
- итерация по каждому ключу из этого объединения[K in keyof T]: ...
- для каждого ключа K создаётся свойство с тем же именем: 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
могут работать как итераторы при использовании с объединениями (union types).
Синтаксис:
type ConditionalType<T> = T extends Condition ? TrueType : FalseType
Как понимать запись:
T extends Condition
- проверка: соответствует ли тип T условию Condition? TrueType : FalseType
- тернарный оператор: если да, то TrueType, иначе FalseType- Когда 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
используется в условных типах для извлечения частей других типов. Хотя сам по себе не является итератором, он часто применяется с дистрибутивными условными типами.
Синтаксис:
type InferredType<T> = T extends Pattern<infer R> ? R : DefaultType;
Как понимать запись:
infer R
- объявляет переменную типа R, которая будет выведена из структуры TT extends Pattern<infer R>
- проверяет, соответствует ли T шаблону и извлекает часть типа? 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]>
}
Как понимать запись:
[K in keyof T]
- итерация по всем ключам TT[K] extends object ? ... : ...
- проверка: является ли значение объектом- Если значение - объект, рекурсивно применяем тот же тип к нему
- Если не объект, применяем другую трансформацию
Пример:
// Глубокий 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`
Как понимать запись:
T extends string
- ограничивает T строковым типом`prefix-${T}-suffix`
- создает шаблон строки с подстановкой T- Если 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 → Для каждого свойства-объекта: рекурсивный вызов → Для примитивных свойств: прямая трансформация → Сборка результатов
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
// }
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"
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.