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

Загружаем научный разбор

Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.

Каталог статейМатериал и источники

Типы как множества значений: полезная модель и её границы в TypeScript

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

Проверяем модель типов как множеств значений: union, intersection, unknown и never; контрпримеры с any, распределением условных типов и мутацией общих ссылок.

Если воспринимать тип как список доступных свойств, объединение типов выглядит странно. После записи A | B допустимых операций иногда становится меньше, а после A & B — больше. Кажется, что названия перепутаны. Противоречие исчезает, если сначала рассматривать множества значений: объединение разрешает больше возможных входов, а пересечение требует одновременно выполнить больше условий. Список безопасных действий является следствием этого выбора.

Однако TypeScript — практический анализатор JavaScript, и модель множеств не является полным формальным описанием его совместимости. Мы проверим, где аналогия даёт точное предсказание, а где нужно вернуться к правилам компилятора. Это авторское объяснение с контрпримерами, а не новая теория типов. Основы синтаксиса разобраны в [уроке об объединениях](/lessons/typescript/typescript-for-developers/typescript-unions); отличие структурной совместимости от номинальной уже обсуждается в [отдельной статье](/research/typescript-structural-typing-generics).

Что именно лежит внутри множества

Для начала выберем конечные литеральные типы. Пусть DraftMode допускает строки 'draft' и 'review', а VisibleMode'review' и 'published'. Здесь можно буквально перечислить значения и проверить принадлежность каждого без оговорок о свойствах объектов.

type DraftMode = 'draft' | 'review';
type VisibleMode = 'review' | 'published';
type EitherMode = DraftMode | VisibleMode;
type SharedMode = DraftMode & VisibleMode;

const shared: SharedMode = 'review';
// @ts-expect-error: draft не принадлежит пересечению
const outside: SharedMode = 'draft';
const allowed: EitherMode = 'published';

Предсказание модели: объединение содержит три разные строки, пересечение — одну. Повторение 'review' не создаёт вторую копию значения. Такой опыт удобен как отправная точка: прежде чем применять аналогию к сложному generic, проверяем, что правильно понимаем операции на маленьком пространстве состояний.

Handbook определяет union через возможные значения составляющих типов и отдельно объясняет, почему операции должны подходить каждому варианту. Именно значения объединяются; свойства не склеиваются механически. TypeScript: Everyday Types.

Более широкий вход означает меньше безусловных возможностей

Представим функцию, которая получает строку или число. В первом случае возможно преобразование регистра, во втором — числовая операция. Пока конкретный случай неизвестен, выбрать специфическое действие без проверки нельзя. Это не недостаток union, а честное выражение неопределённости на входе.

function display(value: string | number): string {
  if (typeof value === 'string') return value.toUpperCase();
  return value.toFixed(1);
}
console.log(display('ready'), display(8)); // READY 8.0

Условие делит исходные возможности на ветви. В одной остаются строки, в другой — числа. Не происходит изменения объекта или преобразования значения в новый тип во время исполнения: исполняется обычная проверка JavaScript, а анализатор использует её для уточнения знания. Такая формулировка особенно полезна при ревью: спрашиваем, какое наблюдение обосновывает действие, а не какой синтаксис заставит исчезнуть ошибку.

Возьмём альтернативный подход — привести всё к string утверждением типа. Тогда исчезнет диагностический сигнал, но число не приобретёт метод строки. Модель множества допустимых входов по-прежнему говорит, что числовой случай возможен. Значит, доказательство безопасности отсутствует; изменилось только сообщение компилятора. Различие между знанием и принятым на доверие утверждением важно сохранять даже в небольшом приложении.

Пересечение объектов сужает множество, расширяя требования

Рассмотрим Named = { name: string } и Timed = { milliseconds: number }. Значение пересечения должно удовлетворять обоим требованиям. Объект с двумя полями подходит. Объект только с именем уже недостаточен. В таком смысле дополнительных требований больше, а допустимых значений меньше.

type Named = { name: string };
type Timed = { milliseconds: number };
const task: Named & Timed = { name: 'parse', milliseconds: 12 };
// @ts-expect-error: отсутствует второе обязательное поле
const incomplete: Named & Timed = { name: 'parse' };

Множество значений типа Named при структурной совместимости не ограничено объектами ровно с одним полем. Поэтому нет противоречия в том, что task подходит также переменной Named. «Известно наличие имени» не означает «известно отсутствие всех остальных свойств». Правила дополнительных свойств свежего литерала — отдельная проверка компилятора, а не доказательство точного состава объекта при исполнении. TypeScript: Type Compatibility.

Это уточнение предотвращает практическую ошибку в работе с ответами сервера. Тип краткого представления описывает используемые поля, но сам по себе не удаляет лишние данные из объекта. Математическая картинка помогает понять направление ограничения, однако для контроля передаваемых данных всё равно нужна реальная проекция. Вопрос о форме значения и вопрос о его сериализации нельзя решить одним знаком пересечения.

unknown и never задают полезные крайние случаи

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

Для обычных типов работают наглядные соотношения: T | never сохраняет T, T & never даёт never, T & unknown сохраняет T, а T | unknown становится unknown. Они отражают пустое множество и универсальную область. Особое поведение any в эти рассуждения пока не включаем. TypeScript 3.0: unknown.

Но never полезен не как декоративный символ в формуле. Пусть приложение должно обработать два состояния загрузки. После двух соответствующих ветвей не должно оставаться третьего допустимого случая.

type LoadState =
  | { kind: 'loading' }
  | { kind: 'ready'; count: number };

function describe(state: LoadState): string {
  switch (state.kind) {
    case 'loading': return 'Ожидание';
    case 'ready': return String(state.count);
    default: {
      const impossible: never = state;
      return impossible;
    }
  }
}

Теперь мысленно добавьте вариант { kind: 'failed'; message: string }. Предсказание: присваивание never перестанет компилироваться, пока новая ветвь не обработана. Это содержательная проверка полноты модели. Она зависит от честности исходного типа и не превращает произвольные внешние данные в проверенное состояние. TypeScript: Exhaustiveness checking.

Проверка алгебры может неожиданно проверять распределение

Разработчик решает написать условие «является ли тип пустым» и получает странный результат. Причина не в том, что пустое множество ведёт себя нелогично, а в особом правиле вычисления conditional types.

type NaiveIsNever<T> = T extends never ? true : false;
type IsNever<T> = [T] extends [never] ? true : false;

type A = NaiveIsNever<never>; // never
type B = IsNever<never>; // true
const confirmation: B = true;

Проверка открытого параметра типа распределяется по вариантам union. Для never в такой конструкции нет составляющих, из которых получился бы результат true; итог остаётся never. Обёртка в кортеж меняет проверяемую конструкцию и выключает это распределение. Поэтому фраза «extends означает включение множеств» недостаточна для чтения произвольного conditional type. TypeScript: Conditional Types.

Полезный метод диагностики — сначала убрать условный тип и проверить присваивание конкретного значения, затем отдельно проверить распределение. Иначе можно приписать неожиданность базовому отношению типов, хотя её создала форма метапрограммы. Такой подход переносится на утилиты фильтрации вариантов и на преобразования union в массивы.

Где аналогия ломается: any и изменяемые ссылки

any нельзя без потерь считать ещё одним названием универсального множества. Через него разрешены операции и присваивания, которые unknown потребовал бы обосновать. Например, значение any можно присвоить строковой переменной, не доказав, что оно является строкой. Это обход части проверки, поэтому цепочка рассуждений «сначала широкий тип, затем автоматически более узкий» перестаёт быть доказательством.

Есть и более тонкая граница без явного any. У изменяемых объектов важны не только допустимые значения сейчас, но и действия других владельцев ссылки. TypeScript допускает некоторые несостоятельные с точки зрения полной безопасности операции ради практической совместимости JavaScript. Документация прямо отмечает эту особенность дизайна. TypeScript: Soundness.

const strings: string[] = ['first'];
const wider: (string | number)[] = strings;
wider.push(7);
console.log(typeof strings[1]); // number

Пример компилируется: более широкий взгляд на массив позволяет записать число в тот же объект. Значит, постоянное утверждение «все элементы strings являются строками» уже нельзя получить только из исходной аннотации. Здесь математическая картинка неподвижного множества значений упускает изменение общего контейнера во времени. Она остаётся полезной для анализа, если явно добавить условие о допустимых мутациях и владельцах ссылок.

В интерфейсе, который лишь читает элементы, readonly помогает ограничить операции через конкретный параметр. Но он не замораживает объект для всех остальных владельцев. Для полной картины нужны договор об изменениях, границы API и проверки поведения. Расширять вывод от локального запрета push до глобальной неизменяемости было бы тем же логическим скачком.

Как превратить аналогию в рабочий инструмент

Комплект примеров автоматически проверен 8 сентября 2026 года на TypeScript 5.9.3 и 6.0.3 с strict, exactOptionalPropertyTypes и noUncheckedIndexedAccess. Отрицательные случаи дали ожидаемую диагностику, а исполнение показало READY 8.0 и число в массиве через общую ссылку. Таким образом, проверены и полезные предсказания аналогии, и конкретный контрпример к слишком сильному выводу о неизменном составе массива.

Полезен и контроль направления включения. Начните с типа, допускающего одну строку, затем расширьте его второй строкой. Значение узкого типа должно подходить потребителю широкого типа, но обратная передача потребует дополнительного основания. После этого выполните аналогичный опыт с объектом, добавляя обязательное поле: направление изменения множества окажется противоположным числу перечисленных свойств. Такая пара опытов проверяет главное содержание аналогии и помогает заметить, когда слова «шире» и «уже» используются только по внешнему виду объявления.

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

Возьмите модель загрузки из статьи и добавьте состояние отмены. Сначала перечислите допустимые варианты в таблице, затем напишите функцию отображения и проверьте её полноту через never. После этого попробуйте передать объект с неизвестным kind через вход типа unknown: понадобится отдельная проверка данных. Это показывает три уровня рассуждения — множество допустимых состояний, полноту обработки и доверие к источнику.

Если объяснение типа не выдерживает такой небольшой опыт, полезно уменьшить пример до литералов и убрать any, assertions и мутации. Затем возвращайте усложнения по одному. [Урок об unknown и never](/lessons/typescript/typescript-for-developers/typescript-any-unknown-never), [урок о narrowing](/lessons/typescript/typescript-for-developers/typescript-narrowing) и [урок об условных типах](/lessons/typescript/typescript-for-developers/typescript-conditional-types) дают практику для этих шагов. Модель множеств ценна, когда помогает сформулировать проверяемое утверждение и вовремя заметить недостающую предпосылку.

Источники

Формат и права

Формат
Авторский разбор

Атрибуция

Авторский русскоязычный разбор. Перечисленные первичные источники использованы для проверки технических утверждений и библиографии; материал не является переводом или перепечаткой источников. Собственные примеры и рассуждения отделены от опубликованных эмпирических результатов.

Код, данные и иллюстрации

Иллюстрации, таблицы и код первоисточников не воспроизводятся. Примеры и текстовые схемы созданы для этой статьи. Ссылки и библиографические сведения не предоставляют прав на перепубликацию материалов источников.