类型断言(Type Assertion)
TypeScript 类型检测无法做到绝对智能,毕竟程序不能像人一样思考。有时会碰到我们比 TypeScript 更清楚实际类型的情况,比如下面的例子:
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find((num) => num > 2); // 提示 ts(2322)
其中,greaterThan2 一定是一个数字(确切地讲是 3),因为 arrayNumber 中明显有大于 2 的成员,但静态类型对运行时的逻辑无能为力。
在 TypeScript 看来,greaterThan2 的类型既可能是数字,也可能是 undefined,所以上面的示例中提示了一个 ts(2322) 错误,此时我们不能把类型 undefined 分配给类型 number。
不过,我们可以使用一种笃定的方式——类型断言(类似仅作用在类型层面的强制类型转换)告诉 TypeScript 按照我们的方式做类型检查。
比如,我们可以使用 as 语法做类型断言,如下代码所示:
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find((num) => num > 2) as number;
又或者是使用尖括号 + 类型的格式做类型断言,如下代码所示:
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = <number>arrayNumber.find((num) => num > 2);
以上两种方式虽然没有任何区别,但是尖括号格式会与 JSX 产生语法冲突,因此我们更推荐使用 as 语法。
注意:类型断言的操作对象必须满足某些约束关系,否则我们将得到一个 ts(2352) 错误,即从类型“源类型”到类型“目标类型”的转换是错误的,因为这两种类型不能充分重叠。
我一度喜欢用“指鹿为马”来形容类型断言,但其实也不够准确。
从物种类型上看,鹿和马肯定不能转换,虽然它们都是动物(继承自同一个父类),但是鹿有“角属性”,马有“鬃毛属性”,所以两者不能充分重叠。
如果我们把它换成“指白马为马”“指马为白马”,就可以很贴切地体现类型断言的约束条件:父子、子父类型之间可以使用类型断言进行转换。
注意:这个结论完全适用于复杂类型,但是对于 number、string、boolean 原始类型来说,不仅父子类型可以相互断言,父类型相同的类型也可以相互断言,比如 1 as 2、'a' as 'b'、true as false(这里的 2、'b'、false 被称之为字面量类型),反过来 2 as 1、'b' as 'a'、false as true 也是被允许的(这里的 1、'a'、true 是字面量类型),尽管这样的断言没有任何意义。
另外,any 和 unknown 这两个特殊类型属于万金油,因为它们既可以被断言成任何类型,反过来任何类型也都可以被断言成 any 或 unknown。因此,如果我们想强行“指鹿为马”,就可以先把“鹿”断言为 any 或 unknown,然后再把 any 和 unknown 断言为“马”,比如鹿 as any as 马。
我们除了可以把特定类型断言成符合约束添加的其他类型之外,还可以使用“字面量值 + as const”语法结构进行常量断言,具体示例如下所示:
/** str 类型是 '"str"' */
let str = "str" as const;
/** readOnlyArr 类型是 'readonly [0, 1]' */
const readOnlyArr = [0, 1] as const;
还有一种特殊非空断言,即在值(变量、属性)的后边添加 '!' 断言操作符,它可以用来排除值为 null、undefined 的情况,具体示例如下:
let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // ts(2531)
对于非空断言来说,我们同样应该把它视作和 any 一样危险的选择。
在复杂应用场景中,如果我们使用非空断言,就无法保证之前一定非空的值,比如页面中一定存在 id 为 feedback 的元素,数组中一定有满足 > 2 条件的数字,这些都不会被其他人改变。而一旦保证被改变,错误只会在运行环境中抛出,而静态类型检测是发现不了这些错误 的。
所以,我们建议使用类型守卫来代替非空断言,比如如下所示的条件判断:
let mayNullOrUndefinedOrString: null | undefined | string;
if (typeof mayNullOrUndefinedOrString === "string") {
mayNullOrUndefinedOrString.toString(); // ok
}
类型推断
在 TypeScript 中,类型标注声明是在变量之后(即类型后置),它不像 Java 语言一样,先声明变量的类型,再声明变量的名称。
使用类型标注后置的好处是编译器可以通过代码所在的上下文推导其对应的类型,无须再声明变量类型,具体示例如下:
{
let x1 = 42; // 推断出 x1 的类型是 number
let x2: number = x1; // ok
}