qrcode

加入前端交流群,阿里、腾讯和京东大佬都在的群里

TS 如何进行完整性检查

创建了一个 “重学TypeScript” 的微信群,想加群的小伙伴,加我微信 “semlinker”,备注 “1” 。阿里、京东、腾讯的大佬都在群里等你哟。

semlinker/awesome-typescript 1.6K

一、never 类型

在 TypeScript 中,never 类型表示的是那些永不存在的值的类型。 例如, never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。此外,变量也可能是 never 类型,当它们被永不为真的类型保护所约束时。为了让大家更好的理解 never 类型,我们来举一些实际的例子。

在定义变量时,可以设置变量的类型为 never 类型:

1
let foo: never; // 定义never类型的变量

never 类型是任何类型的子类型,也可以赋值给任何类型:

1
2
3
let bar: string = (() => {
throw new Error('TypeScript never');
})();

然而,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外)。 即使 any 也不可以赋值给 never

1
2
3
4
5
6
let baz: never = 123; // 赋值失败,number类型不能赋值给never类型的变量

// 定义never类型变量,接收返回值类型为never类型的函数返回值
let bar: never = (() => {
throw new Error('TypeScript never');
})();

另外,对于死循环的函数或执行时总会抛出异常的函数来说,函数对应的返回值类型也是 never 类型,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}

// 推断的返回值类型为never
function fail() {
return error("Some error happened");
}

// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
while (true) {}
}

相信对于刚接触 never 类型的大多数读者来说,看到这里时,心中都会有疑惑 —— never 类型到底有什么用?在 TypeScript 中,可以利用 never 类型的特性来实现完整性检查。

二、利用异常机制实现完整性检查

考虑以下枚举:

1
2
3
4
enum NoYes {
No = 'No',
Yes = 'Yes',
}

下面我们可以在 switch 语句中来使用 NoYes 枚举:

1
2
3
4
5
6
7
8
9
10
function toChinese(x: NoYes) {
switch (x) {
case NoYes.No:
return '否';
case NoYes.Yes:
return '是';
default:
throw new UnsupportedValueError(x); // (A)
}
}

在 A 行中,参数 x 的类型被推断为 never 类型,因为我们已经处理了它可能含有的所有值。因此,我们可以在 A 行中实例化以下异常:

1
2
3
4
5
class UnsupportedValueError extends Error {
constructor(value: never) {
super('Unsupported value: ' + value);
}
}

但是,如果我们忘记了其中一个条件分支的话,那么参数 x 的类型就不再是 never 类型了,我们得到了一个静态的错误:

1
2
3
4
5
6
7
8
9
function toChinese(x: NoYes) {
switch (x) {
case NoYes.Yes:
return '是';
default:
// Argument of type 'NoYes.No' is not assignable to parameter of type 'never'.
throw new UnsupportedValueError(x); // Error
}
}

以上的报错信息很明显,因为我们只处理了 NoYes.Yes 的情形,TypeScript 编译器会推断出 default 分支中变量 x 的类型是 NoYes.No 类型,根据前面介绍的 never 类型的知识,我们知道它是不能赋给 never 类型的变量。

如果你想忽略上述错误,则可以使用 // @ts-ignore 来忽略错误:

1
2
3
4
5
6
7
8
9
function toChinese(x: NoYes) {
switch (x) {
case NoYes.Yes:
return '是';
default:
//@ts-ignore: Argument of type 'NoYes.No' is not assignable to parameter of type 'never'. (2345)
throw new UnsupportedValueError(x); // Error
}
}

TypeScript 2.6 支持在 .ts 文件中通过在报错一行上方使用 // @ts-ignore 来忽略错误。

// @ts-ignore 注释会忽略下一行中产生的所有错误。 建议实践中在 @ts-ignore之后添加相关提示,解释忽略了什么错误。

请注意,这个注释仅会隐藏报错,并且我们建议你少使用这一注释。

1.1 好处:也适用于 if 语句

如果我们使用 if 语句,TypeScript 也会警告我们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function toChineseNonExhaustively(x: NoYes) {
if (x === NoYes.Yes) {
return '是';
} else {
// @ts-ignore: Argument of type 'NoYes.No' is not assignable to parameter of type 'never'. (2345)
throw new UnsupportedValueError(x);
}
}

function toChineseExhaustively(x: NoYes) {
if (x === NoYes.No) {
return '否';
} else if (x === NoYes.Yes) {
return '是';
} else {
throw new UnsupportedValueError(x); // Ok
}
}

好了,接下来我们来介绍进行完整性检查的另一种方法。

三、利用返回类型实现完整性检查

除了利用异常机制之外,我们还可以利用返回类型校验,来实现完整性检查。如果我们忘记处理某个条件分支,TypeScript 也会警告我们(因为我们隐式返回 undefined):

1
2
3
4
5
6
7
8
9
10
11
12
enum NoYes {
No = 'No',
Yes = 'Yes',
}

//@ts-ignore: Function lacks ending return statement and return type does not include 'undefined'. (2366)
function toChinese(x: NoYes): string {
switch (x) {
case NoYes.Yes:
return '是';
}
}

以上错误信息的意思是:函数缺少结尾的 return 语句,并且返回类型不包含 undefined 类型。

2.1 缺点:不适用于 if 语句

使用这种方法,即使我们完整地处理了所有情况,我们也还会收到警告:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum NoYes {
No = 'No',
Yes = 'Yes',
}

// @ts-ignore: Function lacks ending return statement and return type does not include 'undefined'. (2366)
function toChineseExhaustive(x: NoYes): string {
if (x === NoYes.Yes) {
return '是';
}
}

// @ts-ignore: Function lacks ending return statement and return type does not include 'undefined'. (2366)
function toChineseExhaustive(x: NoYes): string {
if (x === NoYes.No) {
return '否';
} else if (x === NoYes.Yes) {
return '是';
}
}

对于代码中的 toChineseExhaustive 方法来说,如果我们把函数方法体中的 if 语句换成 switch 语句的话,是不会收到任何警告的:

1
2
3
4
5
6
7
8
function toChineseExhaustive(x: NoYes): string {
switch (x) {
case NoYes.Yes:
return '是';
case NoYes.No:
return '否'
}
}

2.2 比较这两种方法

与使用异常机制相比,该方法有何不同?

  • 好处:实现起来简单
  • 缺点:
    • 运行时无保护,即不会抛出任何异常
    • 不适用于 if 语句

四、总结

本文介绍了 TypeScript 中实现完整性检查的两种方法并通过实际的例子来介绍它们之间的差异。在例子中虽然我们只使用了枚举类型作为演示,但这种模式也适用于其它类型,比如联合类型和可辨识联合。

五、参考资源


欢迎小伙伴们订阅全栈修仙之路,及时阅读 TypeScript、Node/Deno、Angular 技术栈最新文章。

qrcode