Discriminated union в TypeScript: состояния загрузки без несовместимых полей

Discriminated union в TypeScript: состояния загрузки без несовместимых полей
Содержание статьи

Discriminated union в TypeScript объединяет несколько типов объектов с общим полем-признаком. Например, status: "success" означает, что у объекта есть data, а status: "error" связан с message. Проверка status позволяет компилятору выбрать нужный вариант и разрешить доступ только к его полям.

В разборе Mentoring.digital опишем состояние загрузки списка. Такой тип помогает согласовать ветви отображения, но сам не выполняет запросы и не проверяет пришедший с сервера JSON.

Не храните все варианты в одном наборе необязательных полей

type LooseState = {
  loading: boolean;
  data?: string[];
  error?: string;
};

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

Удобнее связать обязательные данные с конкретным состоянием. Название status может быть другим, но значения признака должны различать варианты.

Опишите каждый вариант и обработайте его в switch

type LoadState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; message: string };

function label(state: LoadState<string[]>): string {
  switch (state.status) {
    case "idle": return "Ещё не загружено";
    case "loading": return "Загрузка";
    case "success": return `Найдено: ${state.data.length}`;
    case "error": return state.message;
    default: {
      const unexpected: never = state;
      return unexpected;
    }
  }
}

const ready: LoadState<string[]> = {status: "success", data: ["A"]};
console.log(label(ready));

В ветви success компилятор знает о data. В ветви error он знает о message. В idle и loading этих полей нет. Не приходится писать data! или принудительно приводить объект через as, чтобы скрыть неопределённость модели.

При настройке проекта включите strict. Если TypeScript уже установлен в проекте, файл можно проверить командой npx tsc --strict --noEmit state.ts. Команду выполняйте в проекте с установленным компилятором, чтобы npx не предлагал случайную загрузку другого пакета.

Используйте never для проверки полноты

После обработки всех вариантов state в default получает тип never. Если добавить новый вариант, например {status: "cancelled"}, и забыть соответствующий case, присваивание переменной unexpected перестанет проходить проверку типов.

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

Что проверить в модели и чего она не гарантирует

  • Присвоение {status: "success"} без data должно быть ошибкой типов.
  • Доступ к state.data до проверки status должен быть ошибкой.
  • Пустой массив в success является успешным результатом, а не автоматически ошибкой загрузки.
  • Добавление нового варианта должно требовать обновить полный switch.

Union описывает допустимую форму отдельного состояния, но не разрешённый порядок переходов между состояниями. Если запрещено переходить из idle прямо в success, потребуется логика переходов. Если хотите показывать старые данные во время повторной загрузки, добавьте осмысленный вариант с нужными полями, а не разрушайте все ограничения optional-полями.

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

Похожие статьи по данной теме