Conditional types в TypeScript: как работает extends

Короткий ответ: “типа если type” в TypeScript обычно означает conditional type, то есть условный тип. Он пишется так: T extends U ? A : B. Это похоже на if, но работает не во время выполнения программы, а на этапе проверки типов

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

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

type A = IsString<string>;
type B = IsString<number>;

A будет true, а B будет false

Базовый синтаксис

type Result<T> = T extends условие ? если_да : если_нет;

Пример полезнее:

type ApiResponse<T> = T extends string
  ? { type: "text"; value: string }
  : { type: "json"; value: T };

type TextResponse = ApiResponse<string>;
type UserResponse = ApiResponse<{ id: number; name: string }>;

TypeScript выбирает форму типа в зависимости от того, что передали в T

Это не обычный if

Так нельзя:

type T = if string then number;

И так тоже не работает:

const value = "hello";
type Result = value extends string ? true : false;

Типы работают с типами, а не с обычными runtime-переменными. Если нужно взять тип переменной, используйте typeof:

const value = "hello";

type Result = typeof value extends string ? true : false;

infer: достать часть типа

infer помогает извлечь тип из сложной конструкции:

type ArrayItem<T> = T extends Array<infer Item> ? Item : never;

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

type Item = ArrayItem<User[]>;

Item будет User

Еще пример для Promise:

type PromiseValue<T> = T extends Promise<infer Value> ? Value : T;

type A = PromiseValue<Promise<number>>;
type B = PromiseValue<string>;

A будет number, B будет string

Когда использовать conditional types

Используйте условные типы, когда форма результата зависит от входного типа. Например:

  • достать элемент массива;
  • достать результат Promise;
  • отфильтровать union;
  • описать разные ответы API;
  • сделать utility type.

Но не используйте их там, где достаточно обычного типа или union. Слишком хитрые conditional types ухудшают читаемость

Практический пример: тип ответа функции

Допустим, функция может возвращать разные данные в зависимости от режима:

type Mode = "id" | "full";

type UserResult<TMode extends Mode> = TMode extends "id"
  ? number
  : { id: number; name: string };

function getUser<TMode extends Mode>(mode: TMode): UserResult<TMode> {
  if (mode === "id") {
    return 1 as UserResult<TMode>;
  }

  return { id: 1, name: "Dinar" } as UserResult<TMode>;
}

const id = getUser("id");
const user = getUser("full");

id будет числом, а user — объектом. Такой прием полезен, когда API функции зависит от литерального параметра

Но если пример начинает требовать много as, стоит задуматься: возможно, обычные перегрузки функции будут понятнее для команды

Частые ошибки

Первая ошибка — думать, что conditional type выполнится в runtime. Нет, это только проверка типов

Вторая ошибка — пытаться проверять значение, а не тип. Для значения нужен обычный if, для типа — T extends U ? A : B

Третья ошибка — усложнять типы без пользы. Если команда не может прочитать тип без отдельного разбора, возможно, его стоит упростить

Четвертая ошибка — забывать про распределение по union. Conditional type вида T extends string ? A : B распределяется по union. Иногда это удобно, иногда нужно отключить распределение через [T] extends [string] ? A : B

Самопроверка

Создайте тип:

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

Проверьте:

type A = IsArray<string[]>;
type B = IsArray<string>;

Если A стал true, а Bfalse, принцип “типа если type” понятен

Что почитать дальше по TypeScript

Если нужен общий маршрут по теме, откройте рубрику TypeScript. Для соседних задач пригодятся эти разборы:

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x