Описание состояний интерфейса с помощью TypeScript

На ревью я часто вижу код, который описывает состояния пользовательского интерфейса. Этот код содержит массу условий.

Для примера давайте рассмотрим код вывода списка пользователей.

В этой статье мы будем рассматривать примеры на Angular, но все о чем тут говорится, можно применить при использовании любого фреймворка.

<ng-container *ngIf="isLoading && !error">
  Loading...
</ng-container>
<ul *ngIf="users && users.length && !error">
  <li *ngFor="let user of users"></li>
</ul>
<ng-container *ngIf="!error && !loading && users && !users.length">
  Nothing found
</ng-container>
<ng-container *ngIf="!isLoading && error">
  
</ng-container>

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

Я предпочитаю другой подход. Давайте попробуем порефакторить этот код.

Я читал про теорию конечных автоматов. Конечный автомат принимает конечный набор состояний, и в один момент времени он находится в одном из этих состояний.

Давайте выделим состояния, в которых может находиться список пользователей.

  • Загрузка — необходимо показать прелоадер
  • Пользователи загружены — необходимо вывести список
  • Сервер вернул ошибку — необходимо вывести текст ошибки
  • Список пользователей пуст (пользователи не найдены) — необходимо вывести соответствующее сообщение

Давайте зафиксируем указанные выше состояния в виде типа с применением discriminating union.

type State =
  | { status: 'loading' }
  | { status: 'success', data: IUser[] }
  | { status: 'failed', error: Error }
  | { status: 'not-founded' };

Сделаем тип State универсальным с помощью дженериков.

type State<TSuccessData> =
  | { status: 'loading' }
  | { status: 'success', data: TSuccessData }
  | { status: 'failed', error: Error }
  | { status: 'not-founded' };

type UsersListState = State<IUser[]>;

Вот и все. Теперь можно переписать логику отображения.

<ng-container *ngIf="state.status === 'loading'">
  Loading...
</ng-container>
<ul *ngIf="state.status === 'success'">
  <li *ngFor="let user of state.data"></li>
</ul>
<ng-container *ngIf="state.status === 'not-found'">
  Nothing found
</ng-container>
<ng-container *ngIf="state.status === 'failed'">
  
</ng-container>

Глядя на такой код, можно сразу понять какое состояние описывается. Этот код сам себя документирует. Кроме того, благодаря этому подходу, вы получаете отличные подсказки в вашей IDE.

Пример подсказок в IDE
В state.status хранится не просто строка, а ограниченный набор значений, которые мы описали выше.
Пример подсказок в IDE
TypeScript подсказывает IDE, что при status === 'success', в state доступны только поля status и data.