ЯдроКодаподготовка к экзаменам
Учебная платформа

Загружаем материалы

Подготавливаем материалы и навигацию по разделу.

Условные типы: infer, distributivity и разница между never и [never]

Автор: · Обновлено

Условный тип выбирает результат по совместимости типов. Самая частая неожиданность возникает не в синтаксисе extends, а в том, проверяется ли объединение целиком или выражение…

Условный тип выбирает результат по совместимости типов. Самая частая неожиданность возникает не в синтаксисе extends, а в том, проверяется ли объединение целиком или выражение применяется отдельно к каждому его варианту.

Цель и предпосылки

Нужны базовые generics, объединения и понимание never. Мы научимся предсказывать распределение, отключать его кортежной обёрткой и использовать infer для извлечения информации. Пример проверяется в TypeScript 6 с strict. Здесь нет задачи написать универсальную библиотеку типов: каждый помощник отвечает на один проверяемый вопрос.

Выбор ветви и совместимость

Запись T extends U ? A : B читается как выбор A, если проверяемый тип совместим с U, и B в другом случае. Это не сравнение типов на строгое равенство и не проверка объекта при запуске JavaScript. Например, объект с дополнительными подходящими полями может быть совместим с более короткой формой. Условие живёт в системе типов и исчезает из выполняемой программы.

Основной пример различает три задачи: оставить строковые варианты объединения, проверить, состоит ли всё объединение из строк, и определить сам тип never. Результаты собраны в кортеж с явной проверкой. Чтение кода по шагам полезнее запоминания готового трюка с квадратными скобками.

export {};

type KeepText<T> = T extends string ? T : never;
type AllText<T> = [T] extends [string] ? true : false;
type IsNever<T> = [T] extends [never] ? true : false;
type BrokenIsNever<T> = T extends never ? true : false;

type Selected = KeepText<'draft' | 12 | 'published'>;
const selected: Selected = 'published';
const checks: [AllText<'draft' | 12>, IsNever<never>, IsNever<[never]>, [never]['length']] =
  [false, true, false, 1];
console.log(selected);
console.log(JSON.stringify(checks));

if (false) {
  // @ts-expect-error Числовой вариант отфильтрован.
  const wrong: Selected = 12;
  // @ts-expect-error Распределение пустого union даёт never, а не true.
  const broken: BrokenIsNever<never> = true;
  // @ts-expect-error Кортеж требует один элемент; пустой массив не подходит.
  const missing: [never] = [];
  void [wrong, broken, missing];
}

Вывод — published и [false,true,false,1]. Отдельные присваивания проверяют ожидаемый смысл выражений: если тип результата изменится, компилятор не примет соответствующее значение. Отрицательные строки дополнительно подтверждают, что неподходящие варианты не прошли незаметно.

Почему происходит распределение

Когда слева от extends стоит непосредственно параметр типа, условный тип распределяется по переданному объединению. Для KeepText<'draft' | 12 | 'published'> выполняются три отдельные проверки. Строковые варианты сохраняются, числовой превращается в never, а объединение результатов содержит две строки. Это поведение лежит в основе полезной фильтрации вариантов, которую также выполняют знакомые Extract и Exclude.

Конкретное выражение с union слева не следует автоматически считать распределительным. Важно наличие параметра в соответствующей позиции. Если вы скопировали формулу из помощника и подставили объединение прямо в текст условия, поведение может измениться. Чтобы разобраться, сначала выпишите, что именно стоит слева: параметр, индексированный доступ, кортеж или уже готовое объединение.

Что меняют квадратные скобки

В AllText сравниваются кортежные формы, содержащие рассматриваемые типы. Проверка относится к объединению целиком: присутствие числа делает утверждение «всё является строкой» ложным. Обёртка не преобразует будущие JavaScript-значения в массивы и не меняет данные приложения. Это способ сформулировать другой вопрос на уровне типов.

С never различие особенно заметно. В распределительном условии never ведёт себя как объединение без вариантов: проверять нечего, поэтому и объединять нечего. Получается never, а не выбранная ожидаемая ветка true. Обёртка позволяет проверить сам тип без такого распределения. Так устроен IsNever, который корректно различает never и обычную кортежную форму [never].

[never] не равен never: у кортежного типа есть длина один и обязательная позиция ноль. Значение для этой позиции нельзя нормально построить в проверенном коде, но из этого не следует, что все операции над кортежным типом обязаны совпадать с операциями над never. Особенно важно не путать [never], never[] и []: у массива never[] допустим пустой экземпляр, а пустой кортеж прямо фиксирует нулевую длину.

Infer извлекает часть подходящей формы

infer вводит имя для части типа, найденной при сопоставлении. Например, из сообщения с полем payload можно извлечь тип полезной нагрузки. Если такого поля нет, помощник должен иметь осмысленный запасной результат. В следующем независимом примере выбран never: неподходящий вариант не даёт полезной нагрузки.

export {};

type PayloadOf<T> = T extends { payload: infer P } ? P : never;
type Message = { kind: 'score'; payload: { points: number } } | { kind: 'ping' };
const payload: PayloadOf<Message> = { points: 17 };
console.log(payload.points);

Будет выведено 17. Не следует делать вывод, что такая формула проверит реальный сетевой пакет: она работает с уже объявленной моделью. Для получения типа результата асинхронной функции обычно достаточно встроенных ReturnType и Awaited; собственный условный помощник нужен, когда договор действительно отличается от стандартного.

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

Не принимайте extends за сравнение на равенство. Не добавляйте кортежи автоматически ко всем условиям: можно случайно отключить именно ту фильтрацию, ради которой помощник написан. Не используйте any как обычный предсказуемый элемент рассуждения о множествах: он способен нарушать привычные ожидания о выборе ветвей. Проверьте помощник на never, смешанном union и неподходящем типе отдельно.

Практикум: один список или разные списки

Напишите два помощника для string | number. Первый должен выдавать string[] | number[], второй — (string | number)[]. Подберите значения, показывающие различие: массив только строк подходит обоим, а массив ['код', 7] должен подходить лишь второму. Затем определите ожидаемый результат каждого помощника при передаче never и проверьте его присваиванием.

Разбор: первый помощник распределяет проверку по параметру и создаёт массив для каждой ветви, второй оборачивает проверяемую сторону в кортеж и формирует один массив общего типа. Для пустого объединения первый результат — never; второй при подходящем условии может стать never[], куда допустим пустой массив. Это содержательное отличие поведения, а не вопрос форматирования. Зафиксируйте оба случая отрицательными примерами, чтобы будущий рефакторинг скобок не изменил договор.

Частые вопросы

Квадратные скобки всегда означают проверку never?

Нет. Они создают кортежные типы и могут использоваться для сравнения любого объединения целиком. Проверка never — известное применение этой техники, но та же конструкция помогает отличить «каждый вариант отдельно» от «весь тип удовлетворяет условию».

Можно ли напечатать вычисленный условный тип в консоли?

Сам тип отсутствует в выполняемом JavaScript. Для проверки используйте присваивания, ожидаемые ошибки или просмотр выведенного типа в редакторе. Напечатанный массив в нашем примере — обычное значение, предварительно проверенное на соответствие нужному типу.

Источники