Discriminated unions

Discriminated unions представляют собой мощный инструмент для типизации данных и управления состоянием в приложениях на TypeScript, используемых во фреймворке Fresh. Они позволяют создавать типы, различающиеся по ключевому свойству, что облегчает обработку разнообразных структур данных без явного использования множества условных операторов или проверок типов.


Основная концепция

Discriminated union строится на основе объединения типов, где каждый тип имеет уникальное дискриминирующее свойство. Это свойство, как правило, литерального типа (string или number), которое служит «меткой», однозначно идентифицирующей вариант типа.

Пример:

type Notification =
  | { type: "email"; subject: string; body: string }
  | { type: "sms"; phoneNumber: string; message: string }
  | { type: "push"; title: string; content: string };

В этом примере type является дискриминирующим свойством. Оно позволяет точно определить структуру объекта и корректно обрабатывать данные в зависимости от типа уведомления.


Использование в условных конструкциях

Discriminated unions интегрируются с TypeScript через narrowing — сужение типа в блоках условных операторов. TypeScript автоматически распознаёт конкретный вариант union и предоставляет корректные свойства.

function handleNotification(notification: Notification) {
  switch (notification.type) {
    case "email":
      console.log(`Email: ${notification.subject}`);
      break;
    case "sms":
      console.log(`SMS to ${notification.phoneNumber}`);
      break;
    case "push":
      console.log(`Push notification: ${notification.title}`);
      break;
  }
}

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


Применение в компонентах Fresh

В Fresh компоненты могут принимать props с discriminated unions, что позволяет гибко управлять разными визуальными и функциональными вариантами компонента.

type ButtonProps =
  | { variant: "primary"; onClick: () => void }
  | { variant: "secondary"; href: string };

export default function Button(props: ButtonProps) {
  if (props.variant === "primary") {
    return <button onCl ick={props.onClick} class="btn-primary">Primary</button>;
  } else {
    return <a href={props.href} class="btn-secondary">Secondary</a>;
  }
}

Такой подход минимизирует ошибки, связанные с неверным использованием props, и позволяет TypeScript предупреждать о несоответствиях на этапе компиляции.


Сочетание с генерацией данных на сервере

Fresh активно использует серверный рендеринг и обработку данных через Handlers. Discriminated unions помогают безопасно обрабатывать разные форматы запросов и ответов.

type ApiResponse =
  | { status: "success"; dat a: string[] }
  | { status: "error"; message: string };

export const handler: Handlers<ApiResponse> = {
  async GET(req, ctx) {
    const success = Math.random() > 0.5;
    if (success) {
      return ctx.render({ status: "success", data: ["item1", "item2"] });
    } else {
      return ctx.render({ status: "error", message: "Failed to fetch data" });
    }
  },
};

На клиенте проверка status позволяет безопасно обрабатывать ответ без риска доступа к несуществующим полям.

function renderApiResponse(response: ApiResponse) {
  if (response.status === "success") {
    return response.data.join(", ");
  } else {
    return `Error: ${response.message}`;
  }
}

Расширение и комбинация типов

Discriminated unions можно комбинировать с другими типами, включая интерфейсы и общие (generic) типы, что делает их крайне гибкими. Например, можно создать обобщённый тип уведомлений с дополнительной информацией:

type BaseNotification<T extends string> = { type: T; createdAt: Date };

type ExtendedNotification =
  | BaseNotification<"email"> & { subject: string; body: string }
  | BaseNotification<"sms"> & { phoneNumber: string; message: string };

Такой подход облегчает поддержку и расширение кода при добавлении новых типов уведомлений или других сущностей.


Преимущества использования discriminated unions

  • Безопасность типов: исключает ошибки доступа к полям, не существующим у конкретного варианта.
  • Автодополнение и проверка IDE: редакторы точно знают структуру объекта в каждой ветке условного оператора.
  • Ясная структура данных: дискриминирующее свойство делает код самодокументируемым.
  • Совместимость с Fresh: удобно применять для props компонентов, API-ответов и обработки состояния.

Discriminated unions являются ключевым инструментом при построении безопасных и масштабируемых приложений на Fresh, обеспечивая строгую типизацию и упрощая работу с разнообразными структурами данных.