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

时间:2016-09-09 20:21:18

标签: 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');
}

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

12 个答案:

答案 0 :(得分:63)

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

第一步是写一个函数:

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

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

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

此时,您会看到错误:

return assertUnreachable(c);
       ~~~~~~~~~~~~~~~~~~~~~
       Type "Color.Blue" is not assignable to type "never"

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

请注意,如果您使用strictNullChecks,则return电话前需要assertUnreachable(否则可选)。

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

// 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);
    }
}

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

答案 1 :(得分:9)

typescript-eslint具有“具有联合类型的交换机中的穷举性检查” 规则:
@typescript-eslint/switch-exhaustiveness-check

答案 2 :(得分:6)

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

export class UnreachableCaseError extends Error {
  constructor(val: never) {
    super(`Unreachable case: ${val}`);
  }
}

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

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);
    }
}

我认为它更容易阅读,因为throw子句具有默认的语法高亮显示。

答案 3 :(得分:5)

作为对Ryan答案的一个很好的修改,您可以将never替换为任意字符串,以使错误消息更加人性化。

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

现在,您得到:

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

之所以可行,是因为never可以分配给任何东西,包括任意字符串。

答案 4 :(得分:3)

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);
  }
}

如果您的开关不完整,则会出现 compile 时间错误。

答案 5 :(得分:2)

您无需使用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
    }
}

您将收到以下编译错误:

  

函数缺少结尾的return语句,并且返回类型没有   包括undefined

答案 6 :(得分:2)

为避免打字稿或棉绒警告:

    default:
        ((_: never): void => {})(c);

在上下文中:

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
        default:
            ((_: never): void => {})(c);
    }
}

此解决方案与其他解决方案之间的区别是

  • 没有未引用的命名变量
  • 它不会引发异常,因为Typescript将强制代码never始终执行

答案 7 :(得分:1)

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

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("How did we get here?");
  }
}

您可以在TS Playground的here行动中看到它

答案 8 :(得分:1)

查找缺失案例的最简单方法是激活 TypeScript 对 no implicit returns 的检查。只需在 noImplicitReturns 文件的 true 部分将 compilerOptions 设置为 tsconfig.json

之后您必须从代码中删除 throw new Error 语句,因为它会阻止 TypeScript 编译器抛出错误(因为您的代码已经在抛出错误):

enum Color {
  Red,
  Green,
  Blue
}

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

使用上面的代码,您将有一个隐式返回(因为如果没有大小写匹配,该函数将返回 undefined)并且 TypeScript 的编译器将抛出错误:

<块引用>

TS2366:函数缺少结束 return 语句并且返回类型不包括“undefined”。

我还制作了一个演示视频:https://www.youtube.com/watch?v=8N_P-l5Kukk

另外,我建议缩小函数的返回类型。它实际上不能返回任何 string 而是只返回一组定义的字符串:

function getColorName(c: Color): 'red' | 'blue'

缩小返回类型还可以帮助您找到缺失的案例,因为某些 IDE(如 VS Code 和 WebStorm)会在您有未使用的字段时向您显示。

答案 9 :(得分:0)

在非常简单的情况下,当您只需要按枚举值返回一些字符串时,更容易(IMHO)使用一些常量来存储结果字典而不是使用switch。例如:

enum Color {
    Red,
    Green,
    Blue
}

function getColorName(c: Color): string {
  const colorNames: Record<Color, string> = {
    [Color.Red]: `I'm red`,
    [Color.Green]: `I'm green`,
    [Color.Blue]: `I'm blue, dabudi dabudai`,   
  }

  return colorNames[c] || ''
}

因此,在这里您必须提及常量中的每个枚举值,否则会出现错误,例如,如果缺少Blue:

  

TS2741:类型'{[Color.Red]:string;中缺少属性'Blue'; [Color.Green]:字符串;'但在“记录”类型中为必填项。

但是通常情况并非如此,然后最好像Ryan Cavanaugh提出的那样抛出错误。

当我发现这也行不通时,我也有些沮丧:

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
    }
    return '' as never // I had some hope that it rises a type error, but it doesn't :)
}

答案 10 :(得分:0)

我想添加一个专门用于 tagged union types 的有用变体,它是 switch...case 的一个常见用例。此解决方案产生:

  • 在编译时进行类型检查
  • 运行时检查,因为打字稿不能保证我们没有错误+谁知道数据来自哪里?
switch(payment.kind) {

        case 'cash':
            return reduceⵧprocessꘌcash(state, action)

        default:
            // @ts-expect-error TS2339
            throw new Error(`reduce_action() unrecognized type "${payment?.kind}!`)
    }

“从不”检测是通过取消引用“从不”基本类型来免费实现的。因为如果我们的代码正确就会出现错误,所以我们用 // @ts-expect-error 翻转它,这样如果我们的代码不正确,它就会失败。我提到了错误 ID,以防它很快得到支持。

答案 11 :(得分:-1)

创建自定义函数,而不使用switch语句。

export function exhaustSwitch<T extends string, TRet>(
  value: T,
  map: { [x in T]: () => TRet }
): TRet {
  return map[value]();
}

示例用法

type MyEnum = 'a' | 'b' | 'c';

const v = 'a' as MyEnum;

exhaustSwitch(v, {
  a: () => 1,
  b: () => 1,
  c: () => 1,
});

如果以后将d添加到MyEnum,则会收到错误消息Property 'd' is missing in type ...