Условные типы в TypeScript позволяют создавать типы, зависящие от других типов — аналогично тому, как работают конструкции if‑else в JavaScript.
Это мощная возможность, открывающая путь к сложным преобразованиям типов и программированию на уровне типов.
Ключевые концепции
- Логика на уровне типов: выполнение условных проверок для типов.
- Вывод типов: извлечение и преобразование типов с помощью
infer. - Композиция: сочетание с другими возможностями TypeScript.
- Вспомогательные типы: создание мощных утилит для работы с типами.
Типичные сценарии применения
- Типобезопасная перегрузка функций.
- Преобразование типов ответов API.
- Сложная валидация типов.
- Создание повторно используемых утилит для работы с типами.
- Продвинутый вывод типов.
Базовый синтаксис условных типов
Условные типы используют форму T extends U ? X : Y, что означает:
"Если тип T расширяет (или совместим с) тип U, использовать тип X, иначе — тип Y."
Пример
type IsString<T> = T extends string ? true : false;
// Примеры использования
type Result1 = IsString<string>; // true
type Result2 = IsString<number>; // false
type Result3 = IsString<"hello">;// true (литеральные типы расширяют свои базовые типы)
// Также можно использовать с переменными
let a: IsString<string>; // a имеет тип 'true'
let b: IsString<number>; // b имеет тип 'false'
Условные типы с объединениями
Распределяющие условные типы
Условные типы особенно полезны с типами‑объединениями: они автоматически применяются к каждому элементу объединения.
Пример
type ToArray<T> = T extends any ? T[] : never;
// При использовании с типом‑объединением применяется к каждому его элементу
type StringOrNumberArray = ToArray<string | number>;
// Превращается в ToArray<string> | ToArray<number>
// Что даёт string[] | number[]
// Можно также извлекать конкретные типы из объединения
type ExtractString<T> = T extends string ? T : never;
type StringsOnly = ExtractString<string | number | boolean | "hello">;
// Результат: string | "hello"
Вывод типов с infer
Извлечение типов из сложных структур
Ключевое слово infer позволяет объявить переменную типа внутри условия условного типа, а затем использовать её в "истинной" ветви условия.
Пример
// Извлекаем возвращаемый тип функции
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Примеры
function greet() { return "Hello, world!"; }
function getNumber() { return 42; }
type GreetReturnType = ReturnType<typeof greet>; // string
type NumberReturnType = ReturnType<typeof getNumber>; // number
// Извлекаем тип элемента массива
type ElementType<T> = T extends (infer U)[] ? U : never;
type NumberArrayElement = ElementType<number[]>; // number
type StringArrayElement = ElementType<string[]>; // string
Встроенные условные типы
Утилиты стандартной библиотеки
TypeScript включает несколько встроенных условных типов в стандартной библиотеке:
Пример
// Extract<T, U> — извлекает из T типы, совместимые с U
type OnlyStrings = Extract<string | number | boolean, string>; // string
// Exclude<T, U> — исключает из T типы, совместимые с U
type NoStrings = Exclude<string | number | boolean, string>; // number | boolean
// NonNullable<T> — удаляет null и undefined из T
type NotNull = NonNullable<string | null | undefined>; // string
// Parameters<T> — извлекает типы параметров из функционального типа
type Params = Parameters<(a: string, b: number) => void>; // [string, number]
// ReturnType<T> — извлекает возвращаемый тип из функционального типа
type Return = ReturnType<() => string>; // string
Продвинутые паттерны и техники
Рекурсивные условные типы
Условные типы можно использовать рекурсивно для создания сложных преобразований типов.
Пример
// Глубоко "распаковываем" типы Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;
// Примеры
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<Promise<number>>>; // number
type C = UnwrapPromise<boolean>; // boolean
Цепочки if-else на уровне типов
Можно объединять несколько условий в одну цепочку для сложной типовой логики.
Пример
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
// Использование
type T0 = TypeName<string>; // "string"
type T1 = TypeName<42>; // "number"
type T2 = TypeName<true>; // "boolean"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<Date[]>; // "object"
Условные типы особенно сильны при создании обобщённых утилит и типобезопасных библиотек.
Пример
// Функция, возвращающая разные типы в зависимости от типа входных данных
function processValue<T>(value: T): T extends string
? string
: T extends number
? number
: T extends boolean
? boolean
: never {
if (typeof value === "string") {
return value.toUpperCase() as any; // Утверждение типа нужно из‑за ограничений
} else if (typeof value === "number") {
return (value * 2) as any;
} else if (typeof value === "boolean") {
return (!value) as any;
} else {
throw new Error("Unsupported type");
}
}
// Использование
const stringResult = processValue("hello"); // Возвращает "HELLO" (тип — string)
const numberResult = processValue(10); // Возвращает 20 (тип — number)
const boolResult = processValue(true); // Возвращает false (тип — boolean)
Лучшие практики
Делайте:
- Используйте условные типы для сложных преобразований типов.
- Сочетайте с
inferдля извлечения типов. - Создавайте повторно используемые утилиты для работы с типами.
- Документируйте сложные условные типы.
- Тестируйте крайние случаи в определениях типов.
Не делайте:
- Не злоупотребляйте сложными условными типами, если достаточно простых.
- Не создавайте глубоко вложенные условные типы — они трудны для понимания.
- Не забывайте о влиянии на производительность при очень сложных типах.
- Не используйте условные типы для логики времени выполнения.
Вопросы производительности
- Глубоко вложенные условные типы могут увеличить время компиляции.
- Рассмотрите использование псевдонимов типов для промежуточных результатов.
- Помните об ограничениях глубины рекурсии в TypeScript.