类型谓词是TypeScript中一个有趣的语法特性。虽然它们出现在与返回类型注解相同的位置,但它们看起来更像简短的肯定句子,而不是典型的注解。这使您能够更精确地控制类型检查。

随着TypeScript 5.5的发布,使用类型谓词现在变得更加直观,因为它在许多情况下可以自动推断它们。但是,如果您正在浏览稍微旧一些的代码库,您更有可能经常遇到手写的类型谓词。

在这篇文章中,我们将简要探讨类型谓词是什么以及它们为什么有用。让我们从它们解决的问题开始。

问题

我认为理解类型谓词的最好方法是注意当我们没有它们时会出现的问题:

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

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string | number
  }
  return " ".repeat(padding) + input; // Opps type error here
                 //   ^
                 // string | number
}

在这里,isString的返回类型被设置为boolean,我们将其在名为padLeft的函数中使用,以向输入字符串的左侧添加填充。padding可以是给定的字符串,也可以是特定数量的空格字符。

以下是文本的简体中文翻译,保留了您自定义的分隔符:

您可能想知道为什么我硬编码了返回类型为boolean。这样做是为了说明问题。如果您不添加任何返回类型注释,并使用最新版本的TypeScript,您在这里不会注意到任何问题。现在,请跟我来——我们很快会讨论版本相关的差异。

函数在运行时可以顺利工作,但TypeScript无法对isString进行任何类型缩小。结果是,无论是在if语句内部还是外部,padding的类型仍然是string | number。这导致了与repeat对其第一个参数的期望发生冲突,从而引发了类型错误。

解决方案:引入类型断言

即使您不熟悉“断言”这个术语,您可能之前已经使用过它们。在编程中,断言是简单的返回布尔值以回答是/否问题的函数。几个JavaScript内置的数组方法,如filterfindeverysome,使用断言来帮助决策。

类型断言是使断言更有用于类型缩小的途径。我们可以通过使用类型断言作为返回类型来修复问题:

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

在这里,类型断言是value is string。它传达了三件事:

  • 这个函数是一个断言。所以如果尝试返回除布尔值之外的内容,TypeScript将显示错误。

  • 如果它返回true,那么value是字符串类型。

  • 如果它返回false,那么value不是字符串类型。

类型断言允许你创建用户定义的类型守卫。类型守卫是逻辑检查,使你能够将类型细化为更具体的类型,也就是缩小它们。所以,上述函数也是一个用户定义的类型守卫。

以下是完整代码:

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

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string
  }
  return " ".repeat(padding) + input;
                 //   ^
                 // number
}

在这里,TypeScript在if语句内部和外部正确地缩小了padding的类型。

现在让我们简要看看在TypeScript 5.5之前类型断言是如何工作的,以及这个版本有什么改进。

TypeScript 5.5之前的类型断言

在我们之前的示例中,如果我们不指定任何返回类型,它将被推断为boolean

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

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string | number
  }
  return " ".repeat(padding) + input; // 这里出现了类型错误
                 //   ^
                 // string | number
}

因此,我们得到了与手动编写返回类型 boolean 时相同的错误。这里是上述代码片段的 TypeScript 游乐场链接。去悬停在函数或变量上以获得更好的类型感觉。然后看看如何编写类型谓词解决问题。

如果我们不指定类型谓词,使用诸如 filter 之类的方法也可能导致不正确的类型检测:

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

const numsOrStrings = [1, 'hello', 2, 'world'];
//      ^
//    strings: (string | number)[]

const strings = numsOrStrings.filter(isString);
//      ^
//    strings: (string | number)[]

现在让我们看看 TypeScript 5.5 如何改善这种情况。

TypeScript 5.5 后的类型谓词

TypeScript 5.5 的一个顶级特性是通过分析函数体来推断类型谓词。所以如果你使用的是 TypeScript 5.5 或更高版本,你不需要为 isString 的返回类型编写类型谓词。TypeScript 会为你完成,并且像下面例子中的代码完全没问题:

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

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // 字符串
  }
  return " ".repeat(padding) + input; // 这里出现了类型错误
                 //   ^
                 // 数字
}

const numsOrStrings = [1, 'hello', 2, 'world'];

const strings = numsOrStrings.filter(isString);
//      ^
//    字符串:string[]

const numbers = numsOrStrings.filter((v) => !isString(v));
//      ^
//    数字:number[]

我还没有找到自动推断类型断言令我不满意的情况。如果你发现了,你总是可以手动编写你自己的。

进一步学习

在本文中,我们简要探讨了TypeScript中的类型断言。如果你有兴趣学习更多并了解边缘情况,请参考官方指南:

感谢阅读!下次见!

封面照片背景来自Mona EendraUnsplash上。