Featured image of post 使用受歧视联合的类型安全状态转换Featured image of post 使用受歧视联合的类型安全状态转换

使用受歧视联合的类型安全状态转换

利用联合字段(如“状态”标签)来清晰地分离业务条件并允许 TS 详尽检查。

在使用 TypeScript 的现代前端和后端开发中,确保状态转换期间的类型安全是构建健壮且无崩溃的应用程序的关键要素。在处理异步 API 通信(例如加载、成功和错误状态之间的转换)或复杂的用户交互时,不明确的类型定义很容易导致意外的运行时错误。

在本文中,我们将探讨如何利用 TypeScript 中的受歧视联合(也称为标记联合)的强大功能来安全、优雅且使用最少的样板文件来管理状态。


1. 什么是受歧视工会?

可判别联合是 TypeScript 中的一种模式,它将多个不同的对象类型组合成一个联合类型 (|),其中每个对象类型共享一个公共文字属性 - 称为“判别式”或“标签”。

TypeScript 的编译器可以在条件结构(如 ifswitch 语句)内读取此标记,以自动缩小相应块内对象的类型范围(称为类型缩小)。

受歧视联盟的关键要素:

  1. 多种对象类型组合成一个联合。
  2. 每种类型中都存在共享属性名称(判别式)。
  3. 此共享属性的类型是文字类型(特定的字符串、数字或布尔值)。

2. 简单的方法:单个整体对象

在深入研究可区分联合之前,让我们检查一个反模式:使用包含所有可选字段的单个对象表示。

// Anti-pattern: Optional properties for all possible states
interface FetchState<T> {
  isLoading: boolean;
  data?: T;
  error?: Error;
}

function renderState<T>(state: FetchState<T>) {
  if (state.isLoading) {
    return "Loading...";
  }

  // Even if isLoading is false, there is no guarantee that data or error is present.
  if (state.data) {
    // TypeScript requires a check or safe-navigation because 'data' is optional.
    return `Data: ${JSON.stringify(state.data)}`;
  }

  if (state.error) {
    return `Error: ${state.error.message}`;
  }

  return "Unknown state";
}

这种方法的缺点:

  • 我们可以轻松配置不可能的状态(例如,同时定义 dataerror ,或者当 isLoading 为 false 时两者都是 undefined )。
  • 开发人员必须不断使用可选链接 (?.) 或非空断言运算符 (!) 来访问数据,这破坏了静态类型检查的目的。

3. 实施歧视性工会

让我们使用一组严格定义的状态重新设计我们的状态表示,并通过联合类型组合。

// Define separate interfaces for every distinct status
interface IdleState {
  type: 'idle';
}

interface LoadingState {
  type: 'loading';
}

interface SuccessState<T> {
  type: 'success';
  data: T;
}

interface ErrorState {
  type: 'error';
  error: Error;
}

// Combine them into a union type
type FetchState<T> = IdleState | LoadingState | SuccessState<T> | ErrorState;

在此模型中,type 属性是我们的判别式。让我们看看现在渲染这个状态是多么容易:

function renderState<T>(state: FetchState<T>): string {
  switch (state.type) {
    case 'idle':
      return 'Waiting...';
    case 'loading':
      return 'Loading...';
    case 'success':
      // The compiler guarantees that 'data' is present here. No optional chaining needed.
      return `Data: ${JSON.stringify(state.data)}`;
    case 'error':
      // The compiler guarantees that 'error' is present here.
      return `Error occurred: ${state.error.message}`;
  }
}

4. 安全第一:详尽检查

使用受歧视联合的最大好处之一是能够利用详尽性检查。当您添加新的状态类型时,您希望编译器在您忘记处理它时发出警告。

我们可以使用 never 类型来实现这一点:

function assertNever(value: never): never {
  throw new Error(`Unhandled state: ${JSON.stringify(value)}`);
}

function renderStateWithGuard<T>(state: FetchState<T>): string {
  switch (state.type) {
    case 'idle':
      return 'Waiting...';
    case 'loading':
      return 'Loading...';
    case 'success':
      return `Data: ${state.data}`;
    case 'error':
      return `Error: ${state.error.message}`;
    default:
      // If we add 'canceling' state in the future and forget to add a case for it,
      // TypeScript will raise a compile-time error here because 'state' cannot be reduced to 'never'.
      return assertNever(state);
  }
}

结论

通过在代码库中采用受歧视联合,您将获得:

  1. 零不可能状态:结构上禁止状态的不可能变化。
  2. 可预测的数据访问:消除防御性编程模式(例如非空断言覆盖)。
  3. 编译时安心:详尽检查在重构过程中充当内置的安全网。

无论您是设计 React 减速器操作、Redux 状态还是 API 消息有效负载,可区分联合都是您应该采用的核心 TypeScript 模式。