TypeScript / 38 минут

Union и сужение типов

Обрабатывайте несколько типов безопасными ветвлениями.

В процессе Теория: впереди Практика: впереди Тест: впереди
02 / 12
После этой главы
  • Обрабатывайте несколько типов безопасными ветвлениями.
  • Применить тему в задаче «Union и сужение типов» и объяснить граничные случаи.

Около 8 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Значение с несколькими допустимыми формами

Union A | B означает, что значение может принадлежать одному из указанных типов. Это не объект, который автоматически содержит все операции обоих вариантов. Если параметр имеет string | number, метод toUpperCase нельзя вызвать без доказательства строкового варианта: у числа такого метода нет. До сужения доступны только операции, подходящие всем оставшимся возможностям.

Союзы полезны для состояний интерфейса, результата поиска и вариантов входного контракта. Вместо набора несвязанных boolean они позволяют описать взаимоисключающие сценарии. Хорошо выбранная модель уменьшает число невозможных сочетаний, о которых приходится помнить разработчику. Проверка типов становится способом выразить предметное правило, а не только подписать свойства.

typeof и поток управления

TypeScript анализирует условия и понимает, какие варианты остались в конкретной ветви. Проверка typeof value === 'string' сужает значение до string, а в противоположной ветви исходного string | number остаётся number. Ранний return тоже влияет на последующий анализ: обработанный вариант больше не должен рассматриваться ниже.

TypeScript
function describe(value: string | number): string {
  if (typeof value === 'string') return value.toUpperCase();
  return value.toFixed(2);
}

Не вся логическая проверка одинаково точно выражает контракт. if (value) исключает falsy-значения, включая пустую строку и ноль. Если их нужно сохранить как полноценные варианты, проверяйте конкретный тип или отсутствие явно. Ошибка моделирования truthiness не исправляется тем, что компилятор успешно понял выбранное условие.

Объекты, null и проверки формы

typeof null возвращает 'object', поэтому для объектного ввода проверка должна отдельно исключить null. Массив также является объектом, но Array.isArray позволяет распознать его специально. Оператор in помогает проверить наличие свойства в подходящем объекте, однако наличие имени ещё не гарантирует тип его значения.

TypeScript
function readName(value: unknown): string | null {
  if (typeof value !== 'object' || value === null) return null;
  if (!('name' in value) || typeof value.name !== 'string') return null;
  return value.name;
}

Для более богатой структуры нужны последовательные проверки всех полей, которые потребитель собирается использовать. Нельзя проверить только id, объявить всё значение User и затем без доказательства обратиться к email. Утверждение типа не добавит отсутствующую проверку. Возвращаемый тип должен соответствовать тому, что действительно установлено реализацией.

Discriminated union

Удобный союз объектов имеет общий дискриминант — поле с литеральным значением, различающим варианты. Например, status может быть 'loading', 'success' или 'error'. После проверки status компилятор знает набор свойств соответствующей ветви. Ошибка не должна притворяться успешным состоянием с пустыми данными, если продукт различает эти случаи.

TypeScript
type Result =
  | { status: 'success'; data: string[] }
  | { status: 'error'; message: string };
function render(result: Result): string {
  return result.status === 'success'
    ? result.data.join(', ') : result.message;
}

Такая структура не позволяет случайно создать success без data в проверяемом TypeScript-коде. Но внешний JSON всё равно нужно проверить: сервер может прислать любую строку status. Статическая модель полезна после доверенной границы, а не вместо неё. На входе из сети форма остаётся неизвестной до runtime-валидации.

never и полнота вариантов

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

TypeScript
function unreachable(value: never): never {
  throw new Error(`Необработанный вариант: ${String(value)}`);
}
type Mode = 'read' | 'write';
function permission(mode: Mode): string {
  switch (mode) {
    case 'read': return 'Чтение';
    case 'write': return 'Запись';
    default: return unreachable(mode);
  }
}

Не используйте приведение as never, чтобы замолчать сигнал неполноты: оно уничтожит смысл проверки. Вместо этого обработайте новый сценарий или измените контракт осознанно. Типы должны помогать обнаруживать забытые ветви, а не становиться украшением вокруг принудительных утверждений.

Практика двух вариантов

По условию строка преобразуется в её length, а число возвращается без изменения. Это короткая задача на точное сужение: нельзя определять вариант через truthiness, иначе пустая строка и ноль будут мешать рассуждению. Не вызывайте строковый метод у числа и не превращайте все входы в строки, потому что это изменит правило для числовой ветви.

Проверьте пустую строку, обычный текст, ноль и отрицательное число. После решения расширьте пример на {kind:'text',value:string} и {kind:'count',value:number}. Сравните, насколько явный дискриминант помогает читать код, когда вариантов и полей становится больше. Затем добавьте третий вариант и убедитесь, что проверка полноты на never действительно обнаруживает забытый сценарий.

Загружаем редактор…

Музыка: Reverie — Scott Buckley · CC BY 4.0