1 概念及语法
类型断言(Type Assertion)即手动指定一个值的类型。语法如下:
值 as 类型<类型>值
第二种语法在 JSX/TSX 语法中可能与组件 <Component>Text 或泛型混淆,建议使用第一种语法 val as type。
2 用途
2.1 将联合类型断言为具体类型
前面说过,联合类型的变量在未被具体确定为某一类型时,只能访问其联合类型的共有属性和方法。
function getCount(data: any[] | Set<any>): Number {if (Object.prototype.toString.call(data) === "[object Array]") {// ERROR: 类型“any[] | Set<any>”上不存在属性“length”。类型“Set<any>”上不存在属性“length”return data.length;}// ERROR: 类型“any[] | Set<any>”上不存在属性“size”。类型“any[]”上不存在属性“size”return data.size;}
借助类型断言,将联合类型的变量断言为某一具体类型后就能访问对应类型的属性和方法了。
function getCount(data: any[] | Set<any>): Number {
if (Object.prototype.toString.call(data) === "[object Array]") {
return (data as Array<any>).length;
}
return (data as Set<any>).size;
}
❗注意:类型断言实际是“欺骗”编译器,绕过编译器的类型判断,但如果随意进行类型断言,可能造成编译通过运行错误!
2.2 将共有父类断言为具体子类
当有多个类继承统一父类后,使用时需要进行区分的情况下,需要将共有父类断言为具体子类。
如:使用 instanceof 关键字判断类型的继承关系
// 网络错误
class httpErr extends Error {
status: number = 404
}
// 请求错误
class apiErr extends Error {
code: number = 1
}
function isHttpErr(err: Error): boolean {
// 如果是具体“类” 的判断,使用 instanceof 更简单
if (err instanceof httpErr) {
return true;
}
return false;
}
但有时 httpErr 与 apiErr 并不是一个具体的“类”,而是用 TypeScript 中的接口 ( interface ) 表示,就只能使用类型断言来实现了
// 网络错误
interface httpErr extends Error {
status: number
}
// 请求错误
interface apiErr extends Error {
code: number
}
function isHttpErr(err: Error): boolean {
// 如果是接口,就只能用类型断言判断
if (typeof (err as httpErr).status === "number") {
return true;
}
return false;
}
2.3 将任何类型断言为任意值类型 ( any )
理想情况下, TypeScript 中的每个变量类型都准确,当访问一个变量不存在的属性时,就会报错:
const foo: number = 1;
foo.length; // ERROR: 类型“number”上不存在属性“length”。
这正是 TypeScript 的优点。但某些情况,我们十分确定这种写法是必要且不会出错的,如
// 确定下面写法是必要且不会报错的
window.bar = 1; // ERROR: 类型“Window & typeof globalThis”上不存在属性“bar”。
为了使 TypeScript 忽略这里的编译异常,就需要临时将变量断言为任意值类型 ( any )
// 临时将 window 断言为任意值类型 ( any )
(window as any).bar = 1;
❗❗❗注意:因尽量减少将任何类型变量断言为任意值 ( any ) ,它有可能会掩盖真正的编译错误且造成后期维护困难。当然必要时仍然需要使用,这取决与开发者的利弊平衡;
2.4 将任意值类型 ( any ) 断言为具体类型
我们很难避免接触任意值类型 ( any ) ,它有可能产生于接手的历史代码或来源于引入的第三方库,当我们遇到某变量为任意值类型 ( any ) 时,你可以选择无视它,也可以根据自己的需要将它断言为具体类型,提高代码维护性。
3 限制
类型断言有没有什么限制呢?是不是任何一个类型都可以被断言为任何另一个类型呢?
答案是否定的——并不是任何一个类型都可以被断言为任何另一个类型。
具体来说,若 A 兼容 B,那么 A 能够被断言为 B,B 也能被断言为 A 。
如
interface Animal {
name: string
}
interface Cat {
name: string,
run(): void
}
const tom: Cat = {
name: "tom",
run() {
console.log("Tom is runing!");
}
}
tom.name;
tom.run();
const animal: Animal = tom; // 为什么能赋值?
animal.name;
animal.run(); // ERROR: 类型“Animal”上不存在属性“run”。
由于 TypeScript 是结构类型系统,类型之间的是比对最终的结构而忽略定义时的关系,因而上述 Cat 与 Animal 的关系最终等同于:
interface Animal {
name: string
}
interface Cat extends Animal {
run(): void
}
这就能理解为什么 tom 能够赋值给 animal 了:因为面向对象编程中可以将子类的实例赋值给类型为父类的变量。
由此可以理解
- 子类可以断言为父类:因为子类继承并拥有了父类的属性和方法,子类断言为父类后去访问父类的属性和方法不会出现问题。
- 父类可以断言为子类:
进而得出结论:要使得 **A** 能断言为 **B** ,要么 **A** 包含于 **B** ( **B** 为联合类型, **A** 是 **B** 的子集 ),要么 **A** 与 **B** 之间有“继承”关系(父类子类可相互断言),这两种情况统称 **A** 与 **B** 有兼容关系。
4 双重断言
前面学到
- 可以将任何类型断言为任意值类型 (
any) ; - 可以将任意值类型 (
any) 断言为任何类型;
那么是不是可以先将某类型断言为任意值类型 ( any ) 后,再将其断言为其他类型,实现类型的随意转换呢?
如 foo as any as bar ,对于 TypeScript 编译器而言确实能通过编译,但对于程序运行和后期维护,双重断言都极有可能带来错误,不到万不得已,不能使用双重断言。
5 断言 VS 类型转换
类型断言只会影响 TypeScript 的编译过程,编译结束后不会保留类型断言部分代码:
function isBoolean(params: any): boolean {
return params as boolean;
}
console.log(isBoolean(1)); // 打印 1 ,类型断言只影响编译,不影响结果
编译后
function isBoolean(params) {
return params;
}
console.log(isBoolean(1)); // 打印 1 ,类型断言只影响编译,不影响结果
如果要对结果进行类型转换,应该调用类型转换方法:
function isBoolean(params: any): boolean {
return Boolean(params);
}
console.log(isBoolean(1)); // 打印 true ,类型转换影响结果
6 类型断言 VS 类型声明
先看例子:
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData('tom') as Cat;
tom.run();
上述代码从缓存中获取数据,由于 getCacheData 返回的数据类型不确定故定义返回类型为任意值类型 ( any ) ,传入参数 'tom' 后,期望获取的数据为 Cat 类的实例,因此可以将 getCacheData('tom') as Cat 断言,进而 tom 变量明确类型与提示。
当然上面的例子也能用类型声明解决:
const tom: Cat = getCacheData('tom');
tom.run();
编译也不会出错,那么类型断言与类型声明的区别是什么?看下面的例子
class Animal {
name: string
}
class Cat extends Animal {
run(): void {
console.log(`${this.name} is runing!`);
}
}
const animal: Animal = {
name: 'tom'
}
// 类型断言,编译通过
const tom1 = animal as Cat;
tom1.run();
// 类型声明,编译报错:类型 "Animal" 中缺少属性 "run",但类型 "Cat" 中需要该属性。
const tom2: Cat = animal;
tom2.run();
因为
- 类型断言只需要
A与B之间有“继承”(兼容)关系就行; - 类型声明后台赋值,必须要赋值方
animal是被赋值方Cat的子类,显然两者关系错误,进而不能赋值;
故类型声明是比类型断言更加严格的。所以为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的 as 语法更加优雅。
7 类型断言 VS 泛型
前面获取缓存数据的例子还有一个解决办法 —— 泛型 。
function getCacheData<T>(key: string): T {
return (window as any).cache[key];
}
interface Cat {
name: string,
run(): void,
}
const tom = getCacheData<Cat>('tom');
tom.run();
通过给 getCacheData 函数添加了一个泛型 <T> ,我们可以更加规范的实现对 **getCacheData** 返回值的约束,这也同时去除掉了代码中的 any 与 as Cat ,是最优的一个解决方案。
