Български

Научете как TypeScript discriminated unions помагат за изграждане на здрави и типово-безопасни машини на състоянията. Дефинирайте състояния и преходи за по-надежден код.

TypeScript Discriminated Unions: Изграждане на типово-безопасни машини на състоянията

В сферата на софтуерното разработване ефективното управление на състоянието на приложението е от решаващо значение. Машините на състоянията (state machines) предоставят мощна абстракция за моделиране на сложни системи със състояния, като осигуряват предвидимо поведение и опростяват логическото мислене за системата. TypeScript, със своята здрава типова система, предлага фантастичен механизъм за изграждане на типово-безопасни машини на състоянията, използвайки discriminated unions (известни също като tagged unions или алгебрични типове данни).

Какво представляват Discriminated Unions?

Discriminated union е тип, който представя стойност, която може да бъде един от няколко различни типа. Всеки от тези типове, известни като членове на обединението, споделя общо, отличително свойство, наречено дискриминант или таг. Този дискриминант позволява на TypeScript точно да определи кой член на обединението е активен в момента, което дава възможност за мощна проверка на типовете и автоматично довършване на кода.

Представете си го като светофар. Той може да бъде в едно от три състояния: червено, жълто или зелено. Свойството 'цвят' действа като дискриминант, който ни казва точно в кое състояние се намира светофарът.

Защо да използваме Discriminated Unions за машини на състоянията?

Discriminated unions носят няколко ключови предимства при изграждането на машини на състоянията в TypeScript:

Дефиниране на машина на състоянията с Discriminated Unions

Нека илюстрираме как да дефинираме машина на състоянията, използвайки discriminated unions с практически пример: система за обработка на поръчки. Една поръчка може да бъде в следните състояния: Pending (Чакаща), Processing (В обработка), Shipped (Изпратена) и Delivered (Доставена).

Стъпка 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: Създайте типа Discriminated Union

След това създаваме discriminated union, като комбинираме тези индивидуални типове с помощта на оператора `|` (union).


type OrderState = Pending | Processing | Shipped | Delivered;

Сега `OrderState` представлява стойност, която може да бъде `Pending`, `Processing`, `Shipped` или `Delivered`. Свойството `type` във всяко състояние действа като дискриминант, позволявайки на TypeScript да ги разграничава.

Обработка на преходи между състоянията

След като сме дефинирали нашата машина на състоянията, ни е необходим механизъм за преход между състоянията. Нека създадем функция `processOrder`, която приема текущото състояние и действие (action) като вход и връща новото състояние.


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; // No state change

    case "processing":
      if (action.type === "shipOrder") {
        return {
          type: "shipped",
          orderId: state.orderId,
          trackingNumber: action.payload.trackingNumber,
        };
      }
      return state; // No state change

    case "shipped":
      if (action.type === "deliverOrder") {
        return {
          type: "delivered",
          orderId: state.orderId,
          deliveryDate: new Date(),
        };
      }
      return state; // No state change

    case "delivered":
      // Order is already delivered, no further actions
      return state;

    default:
      // This should never happen due to exhaustiveness checking
      return state; // Or throw an error
  }
}

Обяснение

Използване на проверката за изчерпателност

Проверката за изчерпателност на TypeScript е мощна функция, която гарантира, че обработвате всички възможни състояния във вашата машина. Ако добавите ново състояние към обединението `OrderState`, но забравите да актуализирате функцията `processOrder`, TypeScript ще сигнализира за грешка.

За да активирате проверката за изчерпателност, можете да използвате типа `never`. Вътре в `default` клаузата на вашето switch изявление, присвоете състоянието на променлива от тип `never`.


function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    // ... (previous cases) ...

    default:
      const _exhaustiveCheck: never = state;
      return _exhaustiveCheck; // Or throw an error
  }
}

Ако `switch` изявлението обработва всички възможни стойности на `OrderState`, променливата `_exhaustiveCheck` ще бъде от тип `never` и кодът ще се компилира. Въпреки това, ако добавите ново състояние към `OrderState` и забравите да го обработите в `switch` изявлението, променливата `_exhaustiveCheck` ще бъде от различен тип и TypeScript ще хвърли грешка по време на компилация, предупреждавайки ви за липсващия случай.

Практически примери и приложения

Discriminated unions са приложими в широк спектър от сценарии извън простите системи за обработка на поръчки:

Пример: Управление на състоянието на потребителския интерфейс

Нека разгледаме прост пример за управление на състоянието на 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 

Click the button to load data.

; case "loading": return

Loading...

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

Error: {state.message}

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

Този пример демонстрира как discriminated unions могат да се използват за ефективно управление на различните състояния на UI компонент, като се гарантира, че потребителският интерфейс се рендира правилно въз основа на текущото състояние. Функцията `renderUI` обработва всяко състояние по подходящ начин, предоставяйки ясен и типово-безопасен начин за управление на UI.

Най-добри практики за използване на Discriminated Unions

За да използвате ефективно discriminated unions във вашите TypeScript проекти, вземете предвид следните най-добри практики:

Разширени техники

Условни типове

Условните типове могат да се комбинират с discriminated unions, за да се създадат още по-мощни и гъвкави машини на състоянията. Например, можете да използвате условни типове, за да дефинирате различни типове за връщане от функция въз основа на текущото състояние.


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

Тази функция използва просто `if` изявление, но може да бъде направена по-здрава, като се използват условни типове, за да се гарантира, че винаги се връща определен тип.

Помощни типове (Utility Types)

Помощните типове на TypeScript, като `Extract` и `Omit`, могат да бъдат полезни при работа с discriminated unions. `Extract` ви позволява да извлечете конкретни членове от тип обединение въз основа на условие, докато `Omit` ви позволява да премахнете свойства от тип.


// Extract the "success" state from the UIState union
type SuccessState = Extract, { type: "success" }>;

// Omit the 'message' property from the Error interface
type ErrorWithoutMessage = Omit;

Реални примери от различни индустрии

Силата на discriminated unions се простира в различни индустрии и области на приложение:

Заключение

TypeScript discriminated unions предоставят мощен и типово-безопасен начин за изграждане на машини на състоянията. Чрез ясното дефиниране на възможните състояния и преходи можете да създадете по-здрав, поддържан и разбираем код. Комбинацията от типова безопасност, проверка за изчерпателност и подобрено автоматично довършване на кода прави discriminated unions безценен инструмент за всеки TypeScript разработчик, който се занимава със сложно управление на състояния. Възползвайте се от discriminated unions в следващия си проект и изпитайте от първа ръка предимствата на типово-безопасното управление на състояния. Както показахме с разнообразни примери от електронната търговия до здравеопазването и от логистиката до образованието, принципът на типово-безопасното управление на състояния чрез discriminated unions е универсално приложим.

Независимо дали изграждате прост UI компонент или сложно корпоративно приложение, discriminated unions могат да ви помогнат да управлявате състоянието по-ефективно и да намалите риска от грешки по време на изпълнение. Така че, потопете се и изследвайте света на типово-безопасните машини на състоянията с TypeScript!