类型断言(Type Assertion)可以用来手动指定一个值的类型。
语法
值 as 类型
或
<类型>值
建议大家在使用类型断言时,统一使用 值 as 类型 这样的语法
类型断言的用途
将一个联合类型断言为其中一个类型
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法:
function reverse(x:number): number;
function reverse(x:string): string;
function reverse(x:number | string): number | string {
if (typeof x === ‘number’) {
return Number(x.toString().split(‘’).reverse().join(‘’));
} else if (typeof x === ‘string’) {
return x.split(‘’).reverse().join(‘’);
};
};
console.log(reverse(12345));
将一个父类断言为更加具体的子类
当类之间有继承关系时,类型断言也是很常见的:
interface ApiError extends Error {code: number;}interface HttpError extends Error {statusCode: number;}function isApiError(error: Error) {if (typeof (error as ApiError).code === 'number') {return true;}return false;}
将任何一个类型断言为 any
理想情况下,TypeScript 的类型系统运转良好,每个值的类型都具体而精确。
当我们引用一个在此类型上不存在的属性或方法时,就会报错:
const foo: number = 1;foo.length = 1;// index.ts:2:5 - error TS2339: Property 'length' does not exist on type 'number'.
上面的例子中,数字类型的变量 foo 上是没有 length 属性的,故 TypeScript 给出了相应的错误提示。
一方面不能滥用 as any,另一方面也不要完全否定它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡(这也是 TypeScript 的设计理念之一),才能发挥出 TypeScript 最大的价值。
将 any 断言为一个具体的类型
在日常的开发中,我们不可避免的需要处理 any 类型的变量,它们可能是由于第三方库未能定义好自己的类型,也有可能是历史遗留的或其他人编写的烂代码,还可能是受到 TypeScript 类型系统的限制而无法精确定义类型的场景。
遇到 any 类型的变量时,我们可以选择无视它,任由它滋生更多的 any。
我们也可以选择改进它,通过类型断言及时的把 any 断言为精确的类型,亡羊补牢,使我们的代码向着高可维护性的目标发展。
举例来说,历史遗留的代码中有个 getCacheData,它的返回值是 any:
function getCacheData(key: string): any {return (window as any).cache[key];}
那么我们在使用它时,最好能够将调用了它之后的返回值断言成一个精确的类型,这样就方便了后续的操作:
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 之后,立即将它断言为 Cat 类型。这样的话明确了 tom 的类型,后续对 tom 的访问时就有了代码补全,提高了代码的可维护性。
类型断言的限制
从上面的例子中,我们可以总结出:
- 联合类型可以被断言为其中一个类型
- 父类可以被断言为子类
- 任何类型都可以被断言为 any
- any 可以被断言为任何类型
类型断言 vs 类型转换
类型断言只会影响 TypeScript 编译时的类型,类型断言语句在编译结果中会被删除:
function toBoolean(something: any): boolean {return something as boolean;}toBoolean(1);// 返回值为 1
在上面的例子中,将 something 断言为 boolean 虽然可以通过编译,但是并没有什么用,代码在编译后会变成:
function toBoolean(something) {return something;}toBoolean(1);// 返回值为 1
所以类型断言不是类型转换,它不会真的影响到变量的类型。
若要进行类型转换,需要直接调用类型转换的方法:
function toBoolean(something: any): boolean {return Boolean(something);}toBoolean(1);// 返回值为 true
类型断言 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();
我们使用 as Cat 将 any 类型断言为了 Cat 类型。
但实际上还有其他方式可以解决这个问题:
function getCacheData(key: string): any {return (window as any).cache[key];}interface Cat {name: string;run(): void;}const tom: Cat = getCacheData('tom');tom.run();
上面的例子中,我们通过类型声明的方式,将 tom 声明为 Cat,然后再将 any 类型的 getCacheData('tom') 赋值给 Cat 类型的 tom。
这和类型断言是非常相似的,而且产生的结果也几乎是一样的——tom 在接下来的代码中都变成了 Cat 类型。
由于 getCacheData('tom') 是 any 类型,any 兼容 Cat,Cat 也兼容 any,故
const tom = getCacheData('tom') as Cat;
等价于
const tom: Cat = getCacheData('tom');
知道了它们的核心区别,就知道了类型声明是比类型断言更加严格的。
所以为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的 as 语法更加优雅。
类型断言 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();
我们还有第三种方式可以解决这个问题,那就是泛型:
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,是最优的一个解决方案。
