如何在TypeScript中检查开关块是否详尽无遗?

Rya*_*ugh 61 typescript

我有一些代码:

enum Color {
    Red,
    Green,
    Blue
}

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
        // Forgot about Blue
    }

    throw new Error('Did not expect to be here');
}
Run Code Online (Sandbox Code Playgroud)

我忘了处理这个Color.Blue案子,我宁愿遇到编译错误.如何构造我的代码,以便TypeScript将此标记为错误?

Rya*_*ugh 80

为此,我们将使用never类型(在TypeScript 2.0中引入),它表示"不应该"出现的值.

第一步是编写一个函数:

function assertUnreachable(x: never): never {
    throw new Error("Didn't expect to get here");
}
Run Code Online (Sandbox Code Playgroud)

然后在default案例中使用它(或等效地,在交换机外部):

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
    }
    return assertUnreachable(c);
}
Run Code Online (Sandbox Code Playgroud)

此时,您将看到一个错误:

return assertUnreachable(c);
       ~~~~~~~~~~~~~~~~~~~~~
       Type "Color.Blue" is not assignable to type "never"
Run Code Online (Sandbox Code Playgroud)

错误消息表示您忘记包含在详尽开关中的情况!如果你没有多个值,你会看到一个错误,例如Color.Blue | Color.Yellow.

请注意,如果您正在使用strictNullChecks,则需要returnassertUnreachable通话前使用它(否则它是可选的).

如果你愿意,你可以得到一点点发烧友.例如,如果您正在使用区分联合,则可以在断言函数中恢复判别属性以进行调试.它看起来像这样:

// Discriminated union using string literals
interface Dog {
    species: "canine";
    woof: string;
}
interface Cat {
    species: "feline";
    meow: string;
}
interface Fish {
    species: "pisces";
    meow: string;
}
type Pet = Dog | Cat | Fish;

// Externally-visible signature
function throwBadPet(p: never): never;
// Implementation signature
function throwBadPet(p: Pet) {
    throw new Error('Unknown pet kind: ' + p.species);
}

function meetPet(p: Pet) {
    switch(p.species) {
        case "canine":
            console.log("Who's a good boy? " + p.woof);
            break;
        case "feline":
            console.log("Pretty kitty: " + p.meow);
            break;
        default:
            // Argument of type 'Fish' not assignable to 'never'
            throwBadPet(p);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是一个很好的模式,因为您可以获得编译时的安全性,以确保您处理了预期的所有情况.如果你确实获得了一个真正的超出范围的属性(例如,一些JS调用者组成了一个新的species),你可以抛出一个有用的错误信息.

  • 如果将其与字符串联合类型 type Pet = "Cat"|"Dog"|"Fish"` 一起使用,则错误只会表明该字符串不可分配给 never。有什么方法可以获取错误消息中丢失的字符串的名称吗? (4认同)
  • 在启用strictNullChecks的情况下,将函数的返回类型定义为“字符串”是足够的,而根本不需要“ assertUnreachable”函数吗? (2认同)
  • @dbandstra 您当然可以这样做,但作为通用模式, assertUnreachable 更可靠。即使没有“strictNullChecks”,它也能工作,如果您想从开关外部返回 undefined,它也能继续工作。 (2认同)

Car*_*nés 36

基于 Ryan 的回答,我发现这里不需要任何额外的功能。我们可以直接做:

function getColorName(c: Color): string {
  switch (c) {
    case Color.Red:
      return "red";
    case Color.Green:
      return "green";
    // Forgot about Blue
    default:
      const exhaustiveCheck: never = c;
      throw new Error(`Unhandled color case: ${exhaustiveCheck}`);
  }
}
Run Code Online (Sandbox Code Playgroud)

您可以在行动中看到它在这里的TS游乐场

编辑:包括避免“未使用的变量”短绒消息的建议。

  • 在 TS 4.9 中,这简化为“c 永不满足”。没有赋值,也没有未使用的变量。 (8认同)
  • 或者,您可以在抛出的错误中使用该变量,例如:`throw new Error(\`Unexpected: ${_exhaustiveCheck}\`);` (4认同)
  • 我也更喜欢使用变量,而不是创建函数(尽管两者都可能在捆绑时被剥离)。无论如何,你的 linter 可能会抱怨未使用的变量,你必须在它上面添加类似的内容:`// eslint-disable-next-line @typescript-eslint/no-unused-vars` (3认同)
  • 我解决了上面代码中的一些相关(我认为这些可能非常频繁)错误,如下所示:```default: { const pleaseBeExhaustive: never = c; throw new Error(`使用所有颜色 - ${pleaseBeExhaustive 作为字符串}`); }``` (2认同)

huw*_*huw 36

satisfies在 TypeScript 4.9 中,使用关键字可以更容易地实现这一点。

\n
enum Color {\n    Red,\n    Green,\n    Blue\n}\n\nfunction getColorName(c: Color): string {\n  switch(c) {\n      case Color.Red:\n          return \'Red\';\n      case Color.Green:\n          return \'Green\';\n      case Color.Blue:\n          return \'Blue\';\n      default:\n          return c satisfies never;\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

如果您的检查很详尽,c则应始终为 类型never。我们使用关键字将 \xe2\x80\x98assert\xe2\x80\x99 传递给编译器satisfies(实质上,告诉它c应该可分配给never, ,否则会出错)。如果将来向枚举添加新的情况,您将得到一个纯粹的编译时错误。

\n

在底层,该default分支将编译为:

\n
default:\n    return c;\n
Run Code Online (Sandbox Code Playgroud)\n

这是一个文字表达式,只会计算c. 这应该\xe2\x80\x99t对你的代码有影响,但是如果c是,例如,一个类的getter,它将评估默认分支是否曾经运行并可能产生副作用(因为它会在当前接受的回答)。

\n


dre*_*ets 16

typescript-eslint具有“在具有联合类型的开关中进行详尽检查”规则:
@typescript-eslint/switch-exhaustiveness-check

要配置它,请启用规则package.json并启用 TypeScript 解析器。一个适用于 React 17 的示例:

  "eslintConfig": {
    "extends": "react-app",
    "rules": {
      "@typescript-eslint/switch-exhaustiveness-check": "warn"
    },
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
      "project": "./tsconfig.json"
    }
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

  • 无论如何,ESLint“default”规则都需要默认情况,这使得该规则毫无用处,因为默认情况使一切变得详尽无遗。该规则也被认为是昂贵的。请注意,它不适用于枚举(但联合类型无论如何都更好)。 (3认同)

TmT*_*ron 12

我所做的是定义一个错误类:

export class UnreachableCaseError extends Error {
  constructor(val: never) {
    super(`Unreachable case: ${val}`);
  }
}
Run Code Online (Sandbox Code Playgroud)

然后在默认情况下抛出此错误:

function meetPet(p: Pet) {
    switch(p.species) {
        case "canine":
            console.log("Who's a good boy? " + p.woof);
            break;
        case "feline":
            console.log("Pretty kitty: " + p.meow);
            break;
        default:
            // Argument of type 'Fish' not assignable to 'never'
            throw new UnreachableCaseError(dataType);
    }
}
Run Code Online (Sandbox Code Playgroud)

我认为它更容易阅读,因为该throw子句具有默认语法高亮.


Mar*_*oni 11

您无需never在末尾使用或添加任何内容switch

如果

  • 您的switch陈述在每种情况下都会返回
  • 您已打开strictNullChecks打字稿编译标记
  • 您的函数具有指定的返回类型
  • 返回类型不是undefinedvoid

如果您的switch陈述不够详尽,则会出现错误,因为在某些情况下不会返回任何内容。

从您的示例中,如果您这样做

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
        // Forgot about Blue
    }
}
Run Code Online (Sandbox Code Playgroud)

您将收到以下编译错误:

函数缺少结尾的return语句,并且返回类型不包含undefined

  • @SK'当您获取一些外部数据时:例如,您从服务器请求数据,然后将其投射到您的界面。然后编译器不会抱怨,但是当服务器真正发送任何字符串而不是您期望的枚举时,您会收到运行时错误。或者您的应用程序的旧版本在本地存储中存储了一些数据,而新版本则读取这些数据 - 但接口定义已更改。 (4认同)
  • 该代码完全按照描述运行。如果您将忽略该类型并认为`c`可能是任何东西,则根本没有理由使用TypeScript。如果`c`可以为null或未定义,则类型应这样说,然后实现将考虑在内。 (3认同)
  • 这个想法不是**返回undefined,而是创建case语句的缺失分支。这样,您在运行时不会出错,也无需抛出任何东西。 (2认同)
  • 该解决方案可以触发打字稿错误,但错误并不总是很清楚。我更喜欢使用“default”案例,其中未使用的变量类型为“never”,并带有注释解释“嘿伙计,如果您在这里遇到错误,那么您忘记添加案例”。YMMV。 (2认同)

小智 7

作为 Ryan 答案的一个很好的转折,您可以never用任意字符串替换,以使错误消息更加用户友好。

function assertUnreachable(x: 'error: Did you forget to handle this type?'): never {
    throw new Error("Didn't expect to get here");
}
Run Code Online (Sandbox Code Playgroud)

现在,你得到:

return assertUnreachable(c);
       ~~~~~~~~~~~~~~~~~~~~~
       Type "Color.Blue" is not assignable to type "error: Did you forget to handle this type?"
Run Code Online (Sandbox Code Playgroud)

这是有效的,因为never可以分配给任何东西,包括任意字符串。


Bra*_*olt 5

基于RyanCarlos 的回答,您可以使用匿名方法来避免创建单独的命名函数:

function getColorName(c: Color): string {
  switch (c) {
    case Color.Red:
      return "red";
    case Color.Green:
      return "green";
    // Forgot about Blue
    default:
      ((x: never) => {
        throw new Error(`${x} was unhandled!`);
      })(c);
  }
}
Run Code Online (Sandbox Code Playgroud)

如果您的开关不是详尽无遗,您将收到编译时错误。