慎用 TypeScript 感叹号『非空断言』操作符

12,719 阅读3分钟

公众号:JavaScript与编程艺术

TS 中的感叹号,称作『非空断言』操作符,Non-null assertion operator

中文理解:

  • x!x 类型集合中排除 nullundefined 的类型。比如 x 可能是 string | undefind,则 x! 类型缩窄为 string
  • 在类型检测器没法正确推断类型情况下,告知编译器此值不可能为空值(nullundefined但这一切将发生改变 2024-03-28,文末会详述。

何时会用到

什么时候会用到『非空断言』,或者说怎么去了解上面这两句话呢?我们举个例子即一目了然。

function isValid(x: any): boolean {
  return x !== undefined && x !== null;
}

function test(y?: string): string {
  if (isValid(y)) {
    return y.toLowerCase(); // 在 isValid 分支内,y 明明不可能是 undefined,为何还报错『Object is possibly 'undefined'.(2532)』?🤔
  }

  return '';
}

报错信息如下:

image.png

isValid 分支内,y 明明不可能是 undefined,为何还报错『Object is possibly 'undefined'.(2532)』?🤔

原因是 TS 并不具备运行时检测(TS 5.5 即将支持 更新于 2024-03-28),或者仅具备有限的类型接检测能力(文章尾部有利用 TS 静态检查能力的版本,推荐使用,本示例只是演示!的作用),如果 isValid 改成 inline 的则 TS 能知道 y 一定不为空,则不会出现 TS error 了。

function test(y?: string): string {
  if (y !== undefined && y !== null) {
    return y.toLowerCase(); // 类型检查其能正确推断此时 y 不可能为空
  }

  return '';
}

若我们的校验逻辑很复杂,或者我们就是想用现成的函数,怎么办?其实有更好的办法能两全其美,可以通过非空断言主动告知迟钝的 TS 类型检查器该变量不可能为空。

当然我们要为自己的行为负责,如果欺骗 TS 导致 NPE(Null Pointer Exception)那就不是 TS 的锅了。

function test(y?: string): string {
  if (isValid(y)) {
    return y!.toLowerCase(); // y 经过函数校验后不可能是 undefined,我们可通过非空断言告知 TS
  }

  return '';
}

! 可用来帮助 TS 类型检测器做类型判断。但须慎用。

附录1:无需 ! 利用 TS 类型检查的版本

function isValid<T>(x: T): x is NotNil<T> {
  return x !== undefined && x !== null;
}

function test(y?: string): string {
  if (isValid(y)) {
    return y.toLowerCase(); // 此时 y 不会提示报错。因为被 isValid 断言非空
  }

  return '';
}

type NotNil<T> = T extends null | undefined ? never : T

此处巧妙利用了自定义的 NotNil 泛型。其实 TS 内置了相同功能的类型 NonNullable。 更多泛型工具可以参考类型界的 lodash type-fest

学会自定义泛型,同理可以自定义 IDefined

type IDefined<T> = T extends undefined ? never : T

附录 2:TS 5.5 即将支持函数控制流分析函数返回值类型

为什么本文是慎用非空断言操作符,有两点原因:

  1. 正确性需要程序员自己保证!
  2. 脆弱性(brittle),函数实现随时可能发生变化,可能导致和断言不一致!

所以 TS 5.5 决定引入 Type Predicate Inference,一种利用 TypeScript 的控制流分析函数体返回类型的特性。

所以我们无需显示标注类型断言,TS 能通过对函数体控制流分析推导出类型断言(type predicate)。

- function isValid<T>(x: T): x is NotNil<T> {
- function isValid(x: unknown): boolean {
+ function isValid<T>(x: T): {
  return x !== undefined && x !== null;
}

以前我们需要这么写 value is string 告诉 TS 经过函数判断后 value 一定是字符串:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

现在可以直接省略,更符合直觉:

function isString(value: unknown) {
  return typeof value === "string";
}

if (isString(value)) {
  console.log(value); // string
}

甚至我们还可以做很多复杂的逻辑,TS 都能帮助我们自动推导。可以想见如果没有该功能,我们的 type predicate 该多繁琐 😅。

function isObjAndHasIdProperty(value: unknown) {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    typeof value.id === "number"
  );
}

if (isObjAndHasIdProperty(value)) {
  console.log(value.id); // number
}

详见 Type Predicate Inference: The TS 5.5 Feature No One Expected。Type Predicate Inference: TypeScript 5.5 中将包含的功能

参考