Українська

Ознайомтеся з дискримінованими об'єднаннями в TypeScript — потужним інструментом для створення надійних машин стану. Навчіться визначати стани, обробляти переходи та підвищувати надійність коду за допомогою системи типів.

Дискриміновані об'єднання в TypeScript: Створення типобезпечних машин стану

У сфері розробки програмного забезпечення ефективне керування станом додатку має вирішальне значення. Машини стану надають потужну абстракцію для моделювання складних систем зі станами, забезпечуючи передбачувану поведінку та спрощуючи аналіз логіки системи. TypeScript, зі своєю надійною системою типів, пропонує чудовий механізм для створення типобезпечних машин стану за допомогою дискримінованих об'єднань (також відомих як теговані об'єднання або алгебричні типи даних).

Що таке дискриміновані об'єднання?

Дискриміноване об'єднання — це тип, що представляє значення, яке може бути одним із кількох різних типів. Кожен із цих типів, відомих як члени об'єднання, має спільну, унікальну властивість, що називається дискримінантом або тегом. Цей дискримінант дозволяє TypeScript точно визначити, який член об'єднання активний на даний момент, що уможливлює потужну перевірку типів та автодоповнення.

Уявіть собі світлофор. Він може перебувати в одному з трьох станів: червоний, жовтий або зелений. Властивість 'color' діє як дискримінант, точно повідомляючи нам, у якому стані перебуває світлофор.

Чому варто використовувати дискриміновані об'єднання для машин стану?

Дискриміновані об'єднання надають кілька ключових переваг при створенні машин стану в TypeScript:

Визначення машини стану за допомогою дискримінованих об'єднань

Проілюструємо, як визначити машину стану за допомогою дискримінованих об'єднань на практичному прикладі: система обробки замовлень. Замовлення може перебувати в таких станах: Очікує, В обробці, Відправлено та Доставлено.

Крок 1: Визначте типи станів

Спочатку ми визначаємо окремі типи для кожного стану. Кожен тип матиме властивість `type`, що діє як дискримінант, а також будь-які дані, специфічні для цього стану.


interface Pending {
  type: "pending";
  orderId: string;
  customerName: string;
  items: string[];
}

interface Processing {
  type: "processing";
  orderId: string;
  assignedAgent: string;
}

interface Shipped {
  type: "shipped";
  orderId: string;
  trackingNumber: string;
}

interface Delivered {
  type: "delivered";
  orderId: string;
  deliveryDate: Date;
}

Крок 2: Створіть тип дискримінованого об'єднання

Далі ми створюємо дискриміноване об'єднання, комбінуючи ці окремі типи за допомогою оператора `|` (union).


type OrderState = Pending | Processing | Shipped | Delivered;

Тепер `OrderState` представляє значення, яке може бути `Pending`, `Processing`, `Shipped` або `Delivered`. Властивість `type` у кожному стані діє як дискримінант, дозволяючи TypeScript розрізняти їх.

Обробка переходів між станами

Тепер, коли ми визначили нашу машину стану, нам потрібен механізм для переходу між станами. Створимо функцію `processOrder`, яка приймає поточний стан та дію як вхідні дані й повертає новий стан.


interface Action {
  type: string;
  payload?: any;
}

function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    case "pending":
      if (action.type === "startProcessing") {
        return {
          type: "processing",
          orderId: state.orderId,
          assignedAgent: action.payload.agentId,
        };
      }
      return state; // Стан не змінюється

    case "processing":
      if (action.type === "shipOrder") {
        return {
          type: "shipped",
          orderId: state.orderId,
          trackingNumber: action.payload.trackingNumber,
        };
      }
      return state; // Стан не змінюється

    case "shipped":
      if (action.type === "deliverOrder") {
        return {
          type: "delivered",
          orderId: state.orderId,
          deliveryDate: new Date(),
        };
      }
      return state; // Стан не змінюється

    case "delivered":
      // Замовлення вже доставлено, подальші дії неможливі
      return state;

    default:
      // Це ніколи не повинно статися завдяки перевірці на повноту
      return state; // Або викинути помилку
  }
}

Пояснення

Використання перевірки на повноту

Перевірка на повноту в TypeScript — це потужна функція, яка гарантує, що ви обробляєте всі можливі стани у вашій машині стану. Якщо ви додасте новий стан до об'єднання `OrderState`, але забудете оновити функцію `processOrder`, TypeScript повідомить про помилку.

Щоб увімкнути перевірку на повноту, ви можете використовувати тип `never`. У секції `default` вашого оператора `switch` присвойте стан змінній типу `never`.


function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    // ... (попередні кейси) ...

    default:
      const _exhaustiveCheck: never = state;
      return _exhaustiveCheck; // Або викинути помилку
  }
}

Якщо оператор `switch` обробляє всі можливі значення `OrderState`, змінна `_exhaustiveCheck` матиме тип `never`, і код скомпілюється. Однак, якщо ви додасте новий стан до об'єднання `OrderState` і забудете обробити його в операторі `switch`, змінна `_exhaustiveCheck` матиме інший тип, і TypeScript видасть помилку компіляції, повідомляючи вас про пропущений випадок.

Практичні приклади та застосування

Дискриміновані об'єднання застосовуються в широкому діапазоні сценаріїв, окрім простих систем обробки замовлень:

Приклад: Управління станом UI

Розглянемо простий приклад управління станом UI компонента, який отримує дані з API. Ми можемо визначити наступні стани:


interface Initial {
  type: "initial";
}

interface Loading {
  type: "loading";
}

interface Success {
  type: "success";
  data: T;
}

interface Error {
  type: "error";
  message: string;
}

type UIState = Initial | Loading | Success | Error;

function renderUI(state: UIState): React.ReactNode {
  switch (state.type) {
    case "initial":
      return 

Натисніть кнопку, щоб завантажити дані.

; case "loading": return

Завантаження...

; case "success": return
{JSON.stringify(state.data, null, 2)}
; case "error": return

Помилка: {state.message}

; default: const _exhaustiveCheck: never = state; return _exhaustiveCheck; } }

Цей приклад демонструє, як дискриміновані об'єднання можна використовувати для ефективного управління різними станами UI компонента, гарантуючи, що інтерфейс користувача відображається правильно відповідно до поточного стану. Функція `renderUI` обробляє кожен стан належним чином, забезпечуючи чіткий і типобезпечний спосіб управління UI.

Найкращі практики використання дискримінованих об'єднань

Щоб ефективно використовувати дискриміновані об'єднання у ваших проєктах на TypeScript, враховуйте наступні найкращі практики:

Просунуті техніки

Умовні типи

Умовні типи можна поєднувати з дискримінованими об'єднаннями для створення ще більш потужних і гнучких машин стану. Наприклад, ви можете використовувати умовні типи для визначення різних типів повернення для функції на основі поточного стану.


function getData(state: UIState): T | undefined {
  if (state.type === "success") {
    return state.data;
  }
  return undefined;
}

Ця функція використовує простий оператор `if`, але її можна зробити надійнішою за допомогою умовних типів, щоб гарантувати повернення конкретного типу.

Допоміжні типи (Utility Types)

Допоміжні типи TypeScript, такі як `Extract` та `Omit`, можуть бути корисними при роботі з дискримінованими об'єднаннями. `Extract` дозволяє витягувати конкретні члени з типу-об'єднання на основі умови, тоді як `Omit` дозволяє видаляти властивості з типу.


// Витягти стан "success" з об'єднання UIState
type SuccessState = Extract, { type: "success" }>;

// Видалити властивість 'message' з інтерфейсу Error
type ErrorWithoutMessage = Omit;

Реальні приклади в різних галузях

Сила дискримінованих об'єднань поширюється на різні галузі та сфери застосування:

Висновок

Дискриміновані об'єднання в TypeScript надають потужний і типобезпечний спосіб створення машин стану. Чітко визначаючи можливі стани та переходи, ви можете створювати більш надійний, підтримуваний і зрозумілий код. Поєднання безпеки типів, перевірки на повноту та розширеного автодоповнення коду робить дискриміновані об'єднання безцінним інструментом для будь-якого розробника TypeScript, що має справу зі складним управлінням станом. Використовуйте дискриміновані об'єднання у своєму наступному проєкті та відчуйте переваги типобезпечного управління станом на власному досвіді. Як ми показали на різноманітних прикладах від електронної комерції до охорони здоров'я, та від логістики до освіти, принцип типобезпечного управління станом за допомогою дискримінованих об'єднань є універсально застосовним.

Незалежно від того, чи створюєте ви простий UI компонент, чи складний корпоративний додаток, дискриміновані об'єднання допоможуть вам ефективніше керувати станом і зменшити ризик помилок під час виконання. Тож занурюйтесь і досліджуйте світ типобезпечних машин стану з TypeScript!