编写智能 TypeScript 详尽检查

77 阅读2分钟

通过标记联合直接求和通常用作 TypeScript 中的设计模式。此时,相当于模式匹配的处理是通过switch来完成的,我一直在寻找一种方法来智能地保证直接求和的分支在运行时和类型检查时都是穷举的。

我想出了一个聪明的方法来一次解决几个问题,而无需引入辅助函数,我想分享一下。

代码

这就是全部内容

// switch (action.type) { ... default:
  throw new Error(`Unknown type: ${(action as { type: "__invalid__" }).type}`);
// .. }

更多详情如下。

问题

在 TypeScript 中,广泛使用一种设计模式,其中对象具有类型属性并输入固定字符串以实现直接求和。此时,通过switch进行模式匹配对应的处理。

// 表示 Get 或 Put 的类型
type Action = GetAction | PutAction;
type GetAction = {
  type: "Get";
  name: string;
};
type PutAction = {
  type: "Put";
  name: string;
  value: string;
};

function act(action: Action) {
  // 区分 Get 和 Put 案例
  switch (action.type) {
    case "Get":
      console.log(`get(${action.name})`);
      break;
    case "Put":
      console.log(`put(${action.name}, ${action.value})`);
      break;
  }
}

但是,这样使用switch存在以下问题。

  • 当更新 Action 以增加操作类型的数量时,会出现分支遗漏,但在类型检查或执行过程中不会注意到这一点。
  • 即使由于一些错误混入了一个意外的对象,也会发生分支遗漏,但这在运行时不会被注意到。

因此,一直在寻找在类型检查时和运行时检查分支详尽性的方法。

天真的运行时检查

一种天真的方法是以下代码:

  // 区分 Get 和 Put 案例
  switch (action.type) {
    case "Get": /* ... */
    case "Put": /* ... */
    default:
      throw new Error(`Unknown type: ${action.type}`);
  }

这有以下问题:

  • 即使有分支的遗漏,也不会发生类型错误。
  • 只有在某处遗漏分支时才会发生类型错误。

后者是由于将 never 推断为动作的流类型而引起的。理论上,对 never 类型值的任何操作都应该导致 never ,但 TypeScript 禁止对 never 进行某些操作以防止人为错误。 action.type 就作为这样的例子被特别排斥。

现有方法

到目前为止,众所周知,TypeScript 会单独检查表达式的值 never 。例如:

function assertNever(_x: never) {}
assertNever(action);

其他已知方法包括使用 const 类型声明和定义接受 never 的错误子类。

建议的方法

在我提出的方法中,我会编写如下代码:

throw new Error(`Unknown type: ${(action as { type: "__invalid__" }).type}`);

整个代码如下所示:

type Action = GetAction | PutAction;
type GetAction = {
  type: "Get";
  name: string;
};
type PutAction = {
  type: "Put";
  name: string;
  value: string;
};

function act(action: Action) {
  switch (action.type) {
    case "Get":
      console.log(`get(${action.name})`);
      break;
    case "Put":
      console.log(`put(${action.name}, ${action.value})`);
      break;
    default:
      throw new Error(`Unknown type: ${(action as { type: "__invalid__" }).type}`);
  }
}

详尽无遗

如果 switch 确实是穷举的,那么这个位置的动作的流类型将被推断为 never。然后,

action as { type: "__invalid__" }

是从 never{ type: "__invalid__" } 的向上转换并且是允许的。同样在语义上,这样的类型断言是合理的,因为这段代码是无法访问的。

因此,

(action as { type: "__invalid__" }).type

具有类型“__invalid__”

即使您使用 TypeScript-ESLint 的 @typescript-eslint/restrict-template-expressions 规则限制模板字符串中的类型,这也不会导致错误。

并不详尽

在非详尽的情况下,例如,如果您需要案例类型:“删除”但忘记了,则操作流类型为

{ type: "Delete", ... }

这将是。然后,

action as { type: "__invalid__" }

是从{type:\ delete \,...}{type:\ __ __ \ \ \}的铸件。

Typescript基本上是被迫忽略类型检查的东西,但是对于明显的可疑内容而言,是编译错误的。在此示例中,它既不是向上的播放也不是降低的,但是输入类型和输出类型没有共同的部分,因此错误地作为可疑铸件犯了错误。

运行时行为

运行时的行为与“简单执行检查”部分相同。

概括

TypeScript提出了一种明智的方法,该方法不需要助手检查Switch Union是否有标记的Union。