TypeScript中的断言函数

TypeScript 3.7 在类型系统中实现了对断言函数的支持。断言函数是一个在发生意外情况时抛出错误的函数。使用断言签名,我们可以告诉 TypeScript 一个函数应该被视为一个断言函数。

一个例子:document.getElementById()方法

让我们从一个示例开始,在该示例中,我们使用该document.getElementById()方法查找 ID 为“root”的 DOM 元素:

const root = document.getElementById("root");

root.addEventListener("click", e => {
  /* ... */
});

我们正在调用该root.addEventListener()方法以将单击处理程序附加到元素。但是,TypeScript 会报告类型错误:

const root = document.getElementById("root");

// Object is possibly null
root.addEventListener("click", e => {
  /* ... */
});

root变量是 type HTMLElement | null,这就是为什么 TypeScript 在我们尝试调用该root.addEventListener()方法时会报告类型错误“Object is possible null”的原因。为了让我们的代码被认为是类型正确的,我们需要在调用方法之前以某种方式确保root变量是非空且非未定义的root.addEventListener()。对于如何做到这一点,我们有几种选择,包括:

  1. 使用非空断言运算符!
  2. 实现内联空值检查
  3. 实现一个断言函数

让我们看一下这三个选项中的每一个。

使用非空断言运算符

首先,我们将尝试使用非空断言运算符,它在调用!之后被写为后缀运算符:document.getElementById()

const root = document.getElementById("root")!;

root.addEventListener("click", e => {
  /* ... */
});

非空断言运算符!告诉 TypeScript 假设返回的值document.getElementById()是非空且非未定义的(也称为“非空值”)。TypeScript 将排除类型nullundefined我们应用!运算符的表达式的类型。

在这种情况下,document.getElementById()方法的返回类型是HTMLElement | null,所以如果我们应用!运算符,我们会得到HTMLElement结果类型。因此,TypeScript 不再报告我们之前看到的类型错误。

但是,在这种情况下,使用非空断言运算符可能不是正确的解决方法。!当我们的 TypeScript 代码编译为 JavaScript 时,该运算符将被完全删除:

const root = document.getElementById("root");

root.addEventListener("click", e => {
  /* ... */
});

非空断言运算符没有任何运行时表现。也就是说,TypeScript 编译器不会发出任何验证代码来验证表达式实际上是非空的。因此,如果由于找不到匹配元素而document.getElementById()调用返回,我们的变量将保存该值,并且我们调用该方法的尝试将失败。nullrootnullroot.addEventListener()

实施内联空值检查

现在让我们考虑第二个选项并实施内联空检查以验证root变量是否包含非空值:

const root = document.getElementById("root");

if (root === null) {
  throw Error("Unable to find DOM element #root");
}

root.addEventListener("click", e => {
  /* ... */
});

由于我们的 null 检查,TypeScript 的类型检查器会将root变量的类型从HTMLElement | null(在 null 检查之前)缩小到HTMLElement(在 null 检查之后):

const root = document.getElementById("root");

// Type: HTMLElement | null
root;

if (root === null) {
  throw Error("Unable to find DOM element #root");
}

// Type: HTMLElement
root;

root.addEventListener("click", e => {
  /* ... */
});

这种方法比以前使用非空断言运算符的方法安全得多。我们通过抛出带有描述性错误消息的错误来显式处理root变量保存值的情况。null

另外,请注意,这种方法不包含任何特定于 TypeScript 的语法;以上所有内容在语法上都是有效的 JavaScript。TypeScript 的控制流分析了解我们的 null 检查的效果,并root在程序的不同位置缩小变量的类型——不需要显式的类型注释。

实现断言函数

最后,现在让我们看看如何使用断言函数以可重用的方式实现这个空值检查。我们将从实现一个assertNonNullish函数开始,如果提供的值为nullor ,该函数将引发错误undefined

function assertNonNullish(
  value: unknown,
  message: string
) {
  if (value === null || value === undefined) {
    throw Error(message);
  }
}

我们在这里使用参数的unknown类型value允许调用站点传递任意类型的值。我们只是将value参数与值null和进行比较undefined,因此我们不需要要求value参数具有更具体的类型。

以下是我们如何assertNonNullish在之前的示例中使用该函数。我们将root变量和错误消息传递给它:

const root = document.getElementById("root");
assertNonNullish(root, "Unable to find DOM element #root");

root.addEventListener("click", e => {
  /* ... */
});

root.addEventListener()但是,TypeScript 仍然会为方法调用产生类型错误:

const root = document.getElementById("root");
assertNonNullish(root, "Unable to find DOM element #root");

// Object is possibly null
root.addEventListener("click", e => {
  /* ... */
});

如果我们root在调用之前和之后查看变量的类型assertNonNullish(),我们会发现它HTMLElement | null在两个地方都是类型:

const root = document.getElementById("root");

// Type: HTMLElement | null
root;

assertNonNullish(root, "Unable to find DOM element #root");

// Type: HTMLElement | null
root;

root.addEventListener("click", e => {
  /* ... */
});

这是因为 TypeScript 不理解assertNonNullish如果提供的函数为空,我们的函数会抛出错误value。我们需要明确地让 TypeScript 知道该assertNonNullish函数应该被视为一个断言函数,该函数断言该值是非空的,否则它将引发错误。我们可以使用asserts返回类型注释中的关键字来做到这一点:

function assertNonNullish<TValue>(
  value: TValue,
  message: string
): asserts value is NonNullable<TValue> {
  if (value === null || value === undefined) {
    throw Error(message);
  }
}

首先,请注意该assertNonNullish函数现在是一个通用函数。它声明了一个类型参数TValue,我们将其用作value参数的类型;我们还在TValue返回类型注释中使用类型。

asserts value is NonNullable<TValue>返回类型注解就是所谓的断言签名。这个断言签名表明,如果函数正常返回(即,如果它没有抛出错误),它就断言value参数的类型是NonNullable<TValue>. TypeScript 使用这条信息来缩小我们传递给value参数的表达式的类型。

NonNullable<T>类型是在 TypeScript 编译器附带的lib.es5.d.ts类型声明文件中定义的条件类型:

/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = T extends null | undefined ? never : T;

当应用于类型T时,辅助类型会从NonNullable<T>中删除类型null和。这里有一些例子:undefinedT

  • NonNullable<HTMLElement>评估为HTMLElement
  • NonNullable<HTMLElement | null>评估为HTMLElement
  • NonNullable<HTMLElement | null | undefined>评估为HTMLElement
  • NonNullable<null>评估为never
  • NonNullable<undefined>评估为never
  • NonNullable<null | undefined>评估为never

有了我们的断言签名,TypeScript 现在可以在函数调用之后正确地缩小root变量的类型。assertNonNullish()类型检查器知道当root持有一个空值时,该assertNonNullish函数将抛出一个错误。如果程序的控制流通过了assertNonNullish()函数调用,则该root变量必须包含一个非空值,因此 TypeScript 会相应地缩小其类型:

const root = document.getElementById("root");

// Type: HTMLElement | null
root;

assertNonNullish(root, "Unable to find DOM element #root");

// Type: HTMLElement
root;

root.addEventListener("click", e => {
  /* ... */
});

由于这种类型缩小,我们的示例现在可以正确地进行类型检查:

const root = document.getElementById("root");
assertNonNullish(root, "Unable to find DOM element #root");

root.addEventListener("click", e => {
  /* ... */
});

所以在这里我们有了它:一个可重用的assertNonNullish断言函数,我们可以使用它来验证表达式是否具有非空值,并通过从中删除nullundefined类型来相应地缩小该表达式的类型。