Hack Frontend Community

Conditional Types в TypeScript

Что такое Conditional Types?

Conditional Types (Условные типы) — это конструкция в TypeScript, которая позволяет выбирать тип на основе условия. Работают по принципу тернарного оператора, но для типов.

Синтаксис

T extends U ? X : Y

Если тип T можно присвоить типу U, то результат — тип X, иначе — тип Y.


Простой пример

type IsString<T> = T extends string ? true : false;

type A = IsString<string>;   // true
type B = IsString<number>;   // false
type C = IsString<'hello'>;  // true

Как это работает?

  1. Проверяется, является ли T подтипом string
  2. Если да — возвращается true
  3. Если нет — возвращается false

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

Извлечение типа возвращаемого значения

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

function getUserName() {
  return 'John';
}

function getUserAge() {
  return 25;
}

type NameType = ReturnType<typeof getUserName>;  // string
type AgeType = ReturnType<typeof getUserAge>;    // number

Фильтрация типов

type NonNullable<T> = T extends null | undefined ? never : T;

type A = NonNullable<string | null>;        // string
type B = NonNullable<number | undefined>;   // number
type C = NonNullable<boolean | null | undefined>;  // boolean

Проверка на массив

type IsArray<T> = T extends any[] ? true : false;

type A = IsArray<number[]>;     // true
type B = IsArray<string>;       // false
type C = IsArray<[1, 2, 3]>;    // true (tuple тоже массив)

Distributive Conditional Types

Когда условный тип применяется к union type, он распределяется по каждому члену объединения.

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

type A = ToArray<string | number>;
// string extends any ? string[] : never | number extends any ? number[] : never
// string[] | number[]

Как это работает?

ToArray<string | number>
// Распределяется как:
= ToArray<string> | ToArray<number>
= string[] | number[]

Отключение распределения

Используйте квадратные скобки для предотвращения распределения:

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

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

Ключевое слово infer

infer позволяет "извлечь" тип из структуры во время проверки условия.

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

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

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

Извлечение типов аргументов функции

type FirstArg<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;

function test(name: string, age: number) {
  return { name, age };
}

type First = FirstArg<typeof test>;  // string

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

type ElementType<T> = T extends (infer E)[] ? E : T;

type A = ElementType<string[]>;    // string
type B = ElementType<number[]>;    // number
type C = ElementType<boolean>;     // boolean

Вложенные условные типы

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 A = TypeName<string>;      // "string"
type B = TypeName<42>;          // "number"
type C = TypeName<() => void>;  // "function"
type D = TypeName<{}>;          // "object"

Встроенные утилиты на основе Conditional Types

TypeScript предоставляет множество встроенных утилит, реализованных через условные типы.

Exclude

Исключает типы из union:

type Exclude<T, U> = T extends U ? never : T;

type A = Exclude<'a' | 'b' | 'c', 'a'>;  // 'b' | 'c'
type B = Exclude<string | number, string>;  // number

Extract

Извлекает типы из union:

type Extract<T, U> = T extends U ? T : never;

type A = Extract<'a' | 'b' | 'c', 'a' | 'f'>;  // 'a'
type B = Extract<string | number, number>;     // number

NonNullable

Удаляет null и undefined:

type NonNullable<T> = T extends null | undefined ? never : T;

type A = NonNullable<string | null>;  // string
type B = NonNullable<number | undefined | null>;  // number

ReturnType

Извлекает тип возвращаемого значения функции:

type ReturnType<T extends (...args: any) => any> = 
  T extends (...args: any) => infer R ? R : any;

function getUserData() {
  return { name: 'John', age: 25 };
}

type UserData = ReturnType<typeof getUserData>;
// { name: string; age: number; }

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

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

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

interface User {
  name: string;
  address: {
    city: string;
    country: string;
  };
}

type ReadonlyUser = DeepReadonly<User>;
/*
{
  readonly name: string;
  readonly address: {
    readonly city: string;
    readonly country: string;
  };
}
*/

Извлечение ключей определенного типа

type KeysOfType<T, U> = {
  [K in keyof T]: T[K] extends U ? K : never;
}[keyof T];

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

type StringKeys = KeysOfType<User, string>;  // "name" | "email"
type NumberKeys = KeysOfType<User, number>;  // "id" | "age"

Создание Required полей по условию

type RequireKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;

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

type UserWithName = RequireKeys<User, 'name'>;
// { name: string; age?: number; email?: string; }

Практические кейсы

API Response Helper

type ApiResponse<T> = T extends { data: infer D }
  ? D
  : T;

interface SuccessResponse {
  data: { id: number; name: string };
  status: 'success';
}

type Data = ApiResponse<SuccessResponse>;
// { id: number; name: string; }

Flatten Union Types

type Flatten<T> = T extends Array<infer U> ? U : T;

type A = Flatten<string[]>;           // string
type B = Flatten<number[][]>;         // number[]
type C = Flatten<(string | number)[]>; // string | number

3. Promise Chain Type

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<Promise<Promise<Promise<boolean>>>>; // boolean

Function or Value

type ValueOrFunction<T> = T | (() => T);

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

type A = Resolve<string>;        // string
type B = Resolve<() => number>;  // number

Ограничения и особенности

Глубина рекурсии

TypeScript ограничивает глубину рекурсивных типов для предотвращения бесконечных циклов:

// Может привести к ошибке "Type instantiation is excessively deep"
type DeepArray<T, N extends number = 10> = 
  N extends 0 ? T : DeepArray<T[], Decrement<N>>;

Порядок проверки имеет значение

// Порядок важен
type TypeName<T> = 
  T extends any[] ? "array" :      // Сначала проверяем массив
  T extends object ? "object" :    // Потом объект
  T extends string ? "string" :
  "other";

type A = TypeName<string[]>;  // "array" (не "object")

Never в условных типах

type A = never extends string ? true : false;  // true
// never является подтипом любого типа

Сравнение с другими подходами

До Conditional Types

// Приходилось использовать перегрузки
function wrap(x: string): string[];
function wrap(x: number): number[];
function wrap(x: any): any[] {
  return [x];
}

С Conditional Types

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

function wrap<T>(x: T): Wrap<T> {
  return [x] as Wrap<T>;
}

const a = wrap('hello');  // string[]
const b = wrap(42);       // number[]

Типичные ошибки

Забыть про distributivity

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

type A = WrapInArray<string | number>;  // string[] | number[]
// Ожидали (string | number)[], но получили union массивов

// Правильно:
type WrapInArray<T> = [T] extends [any] ? T[] : never;
type B = WrapInArray<string | number>;  // (string | number)[]

Неправильное использование infer

// Неправильно
type Wrong<T> = T extends infer U ? U : never;  // Бессмысленно

// Правильно
type Correct<T> = T extends Promise<infer U> ? U : T;

Сложная логика в одном типе

// Плохо - сложно читать
type Complex<T> = T extends string ? T extends `${infer F}${infer R}` ? F extends 'a' ? true : false : false : false;

// Хорошо - разбить на части
type StartsWithA<T> = T extends `a${string}` ? true : false;
type IsString<T> = T extends string ? true : false;
type Complex<T> = IsString<T> extends true ? StartsWithA<T> : false;

Вывод

Conditional Types:

  • Позволяют создавать гибкие и переиспользуемые типы
  • Основа для многих встроенных утилит TypeScript
  • Распределяются по union типам (distributive)
  • Поддерживают извлечение типов через infer
  • Могут быть рекурсивными (с ограничениями)
  • Критически важны для продвинутой типизации

На собеседовании:

Важно уметь:

  • Объяснить синтаксис T extends U ? X : Y
  • Показать разницу между distributive и non-distributive типами
  • Использовать infer для извлечения типов
  • Привести примеры встроенных утилит
  • Реализовать собственные утилиты на основе условных типов