类型断言能够显式告知类型检查程序当前这个变量的类型,可以进行类型分析地修正、类型。它其实就是一个将变量的已有类型更改为新指定类型的操作,它的基本语法是 as NewType,你可以将 any / unknown 类型断言到一个具体的类型:

  1. let unknownVar: unknown;
  2. (unknownVar as { foo: () => {} }).foo();

还可以 as 到 any 来为所欲为,跳过所有的类型检查:

  1. const str: string = "linbudu";
  2. (str as any).func().foo().prop;

也可以在联合类型中断言一个具体的分支:

  1. function foo(union: string | number) {
  2. if ((union as string).includes("linbudu")) { }
  3. if ((union as number).toFixed() === '599') { }
  4. }

但是类型断言的正确使用方式是,在 TypeScript 类型分析不正确或不符合预期时,将其断言为此处的正确类型:

  1. interface IFoo {
  2. name: string;
  3. }
  4. declare const obj: {
  5. foo: IFoo
  6. }
  7. const {
  8. foo = {} as IFoo
  9. } = obj

这里从 {} 字面量类型断言为了 IFoo 类型,即为解构赋值默认值进行了预期的类型断言。当然,更严谨的方式应该是定义为 Partial 类型,即 IFoo 的属性均为可选的。
除了使用 as 语法以外,你也可以使用 <> 语法。它虽然书写更简洁,但效果一致,只是在 TSX 中尖括号断言并不能很好地被分析出来。你也可以通过 TypeScript ESLint 提供的 consistent-type-assertions 规则来约束断言风格。
需要注意的是,类型断言应当是在迫不得己的情况下使用的。虽然说我们可以用类型断言纠正不正确的类型分析,但类型分析在大部分场景下还是可以智能地满足我们需求的。
总的来说,在实际场景中,还是 as any 这一种操作更多。但这也是让你的代码编程 AnyScript 的罪魁祸首之一,请务必小心使用。

双重断言

如果在使用类型断言时,原类型与断言类型之间差异过大,也就是指鹿为马太过离谱,离谱到了指鹿为霸王龙的程度,TypeScript 会给你一个类型报错:

  1. const str: string = "linbudu";
  2. // 从 X 类型 到 Y 类型的断言可能是错误的,blabla
  3. (str as { handler: () => {} }).handler()

此时它会提醒你先断言到 unknown 类型,再断言到预期类型,就像这样:

  1. const str: string = "linbudu";
  2. (str as unknown as { handler: () => {} }).handler();
  3. // 使用尖括号断言
  4. (<{ handler: () => {} }>(<unknown>str)).handler();

这是因为你的断言类型和原类型的差异太大,需要先断言到一个通用的类,即 any / unknown。这一通用类型包含了所有可能的类型,因此断言到它从它断言到另一个类型差异不大。

非空断言

非空断言其实是类型断言的简化,它使用 ! 语法,即 obj!.func()!.prop 的形式标记前面的一个声明一定是非空的(实际上就是剔除了 null 和 undefined 类型),比如这个例子:

  1. declare const foo: {
  2. func?: () => ({
  3. prop?: number | null;
  4. })
  5. };
  6. foo.func().prop.toFixed();

此时,func 在 foo 中不一定存在,prop 在 func 调用结果中不一定存在,且可能为 null,我们就会收获两个类型报错。如果不管三七二十一地坚持调用,想要解决掉类型报错就可以使用非空断言:

  1. foo.func!().prop!.toFixed();

其应用位置类似于可选链:

  1. foo.func?.().prop?.toFixed();

但不同的是,非空断言的运行时仍然会保持调用链,因此在运行时可能会报错。而可选链则会在某一个部分收到 undefined 或 null 时直接短路掉,不会再发生后面的调用。
非空断言的常见场景还有 document.querySelector、Array.find 方法等:

  1. const element = document.querySelector("#id")!;
  2. const target = [1, 2, 3, 599].find(item => item === 599)!;

为什么说非空断言是类型断言的简写?因为上面的非空断言实际上等价于以下的类型断言操作:

  1. ((foo.func as () => ({
  2. prop?: number;
  3. }))().prop as number).toFixed();

怎么样,非空断言是不是简单多了?你可以通过 non-nullable-type-assertion-style 规则来检查代码中是否存在类型断言能够被简写为非空断言的情况。
类型断言还有一种用法是作为代码提示的辅助工具,比如对于以下这个稍微复杂的接口:

  1. interface IStruct {
  2. foo: string;
  3. bar: {
  4. barPropA: string;
  5. barPropB: number;
  6. barMethod: () => void;
  7. baz: {
  8. handler: () => Promise<void>;
  9. };
  10. };
  11. }

假设你想要基于这个结构随便实现一个对象,你可能会使用类型标注:

  1. const obj: IStruct = {};

这个时候等待你的是一堆类型报错,你必须规规矩矩地实现整个接口结构才可以。但如果使用类型断言,我们可以在保留类型提示的前提下,不那么完整地实现这个结构:

  1. // 这个例子是不会报错的
  2. const obj = <IStruct>{
  3. bar: {
  4. baz: {},
  5. },
  6. };

类型提示仍然存在:
类型断言 - 图1
在你错误地实现结构时仍然可以给到你报错信息:
类型断言 - 图2