我有一些代码:
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将此标记为错误?
答案 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)
在Ryan和Carlos'答案的基础上,您可以使用匿名方法来避免创建单独的命名函数:
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
打字稿编译标记undefined
或void
如果您的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);
}
}
此解决方案与其他解决方案之间的区别是
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 ...