类型断言(Type Assertion)可以用来手动指定一个值的类型。
1. 语法
类型断言有两种写法:
- 值
**as**类型 - <类型>值
建议在使用类型断言时,统一使用as语法,笔者在接下来的阐述中也会使用as语法来进行讲解。
2. 用途
类型断言的常见用途有以下几种:
2.1 联合类型断言
当TypeScript不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法:
interface Cat {name: string;run(): void;}interface Fish {name: string;swim(): void;}function getName(animal: Cat | Fish) {return animal.name;}
而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,比如:
interface Cat {name: string;run(): void;}interface Fish {name: string;swim(): void;}function isFish(animal: Cat | Fish) {if (typeof animal.swim === 'function') {return true;}return false;}// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.// Property 'swim' does not exist on type 'Cat'.
上面的例子中,获取animal.swim的时候会报错。此时可以使用类型断言,将方法isFish中的animal断言成Fish:
function isFish(animal: Cat | Fish) {if (typeof (animal as Fish).swim === 'function') { // 告诉 TS animal 就是 Fish 类型return true;}return false;}
这样就可以解决访问animal.swim时报错的问题了。
需要注意的是,类型断言只能够“欺骗”**TypeScript**编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误:
interface Cat {name: string;run(): void;}interface Fish {name: string;swim(): void;}function swim(animal: Cat | Fish) {(animal as Fish).swim();}const tom: Cat = {name: 'Tom',run() { console.log('run') }};swim(tom);// Uncaught TypeError: animal.swim is not a function`
上面的例子编译时不会报错,但在运行时会报错。原因是(animal as Fish).swim()这段代码隐藏了animal可能为Cat的情况,将animal直接断言为Fish了,而TypeScript编译器信任了我们的断言,故在调用swim()时没有编译错误。
可是swim函数接受的参数是Cat | Fish,一旦传入的参数是Cat类型的变量,由于Cat上没有swim方法,就会导致运行时错误了。
总之,使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。
2.2 接口类型断言
当接口之间有继承关系时,类型断言也是很常见的:
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;}
上面的例子中,我们声明了函数isApiError,它用来判断传入的参数是不是ApiError类型,为了实现这样一个函数,它的参数的类型肯定得是比较抽象的父Error接口类型,这样的话这个函数就能接受父Error接口或它的子接口作为参数了。
但是由于父类Error中没有code属性,故直接获取error.code会报错,需要使用类型断言获取(error as ApiError).code。
有时候接口之间没有继承关系,但是有兼容关系,也可以互相进行类型断言:
interface Animal {name: string;}interface Cat {name: string;run(): void;}function testAnimal(animal: Animal) {return (animal as Cat);}function testCat(cat: Cat) {return (cat as Animal);}
如何确定两个接口之间是否有兼容关系呢?
我们知道,TypeScript是结构类型系统,类型之间的对比只会比较它们最终的结构,而会忽略它们定义时的关系。
在上面的例子中,Cat包含了Animal中的所有属性,除此之外,它还有一个额外的方法run。TypeScript并不关心Cat和Animal之间定义时是什么关系,而只会看它们最终的结构有什么关系——所以它与Cat extends Animal其实是等价的:
interface Animal {name: string;}interface Cat extends Animal {run(): void;}
我们把它换成TypeScript中更专业的说法,即:Animal兼容Cat。
需要注意的是,这里我们使用了简化的父类子类的关系来表达类型的兼容性,而实际上TypeScript在判断类型的兼容性时,比这种情况复杂很多。
总之,若A兼容B,那么A能够被断言为B,B也能被断言为A。
同理,若B兼容A,那么A能够被断言为B,B也能被断言为A。
2.3 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的访问时就有了代码补全,提高了代码的可维护性。
2.4 任何类型断言为 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给出了相应的错误提示。
这种错误提示显然是非常有用的。
但有的时候,我们非常确定这段代码不会出错,比如下面这个例子:
window.foo = 1;// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.
上面的例子中,我们需要将window上添加一个属性foo,但TypeScript编译时会报错,提示我们window上不存在foo属性。
此时我们可以使用as any临时将window断言为any类型:
(window as any).foo = 1;
在
any类型的变量上,访问任何属性都是允许的。
需要注意的是,将一个变量断言为**any**可以说是解决**TypeScript**中类型问题的最后一个手段。它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用**as any**。
上面的例子中,我们也可以通过扩展lib.dom.d.ts文件中的Window接口来解决这个错误:
declare interface Window {foo: any;}
TypeScript会自动将你扩展的Window接口与内置的Window接口进行合并。不过如果只是临时的增加 foo属性,as any会更加方便。
总之,一方面不能滥用**as any**,另一方面也不要完全否定它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡(这也是TypeScript的设计理念之一),才能发挥出TypeScript最大的价值。
3. 双重断言
既然:
- 任何类型都可以被断言为
any any可以被断言为任何类型
那么我们是不是可以使用双重断言as any as Foo来将任何一个类型断言为任何另一个类型呢?
interface Cat {run(): void;}interface Fish {swim(): void;}function testCat(cat: Cat) {return (cat as any as Fish);}
在上面的例子中,若直接使用cat as Fish肯定会报错,因为Cat和Fish互相都不兼容。
但是若使用双重断言,则可以打破「要使得A能够被断言为B,只需要A兼容B或B兼容A即可」的限制,将任何一个类型断言为任何另一个类型。
若你使用了这种双重断言,那么十有八九是非常错误的,它很可能会导致运行时错误。
除非迫不得已,千万别用双重断言。
4. 类型断言 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
5. 类型断言 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类型。
它们的区别,可以通过这个例子来理解:
interface Animal {name: string;}interface Cat {name: string;run(): void;}const animal: Animal = {name: 'tom'};let tom = animal as Cat;
在上面的例子中,由于Animal兼容Cat,故可以将animal断言为Cat赋值给tom。
但是若直接声明tom为Cat类型:
interface Animal {name: string;}interface Cat {name: string;run(): void;}const animal: Animal = {name: 'tom'};let tom: Cat = animal;// index.ts:12:5 - error TS2741: Property 'run' is missing in type 'Animal' but required in type 'Cat'.
则会报错,不允许将animal赋值为Cat类型的tom。
深入的讲,它们的核心区别就在于:
animal断言为Cat,只需要满足Animal兼容Cat或Cat兼容Animal即可animal赋值给tom,需要满足Cat兼容Animal才行
但是Cat并不兼容Animal。
而在前一个例子中,由于getCacheData('tom')是any类型,any兼容Cat,Cat也兼容any,故
const tom = getCacheData('tom') as Cat;
等价于
const tom: Cat = getCacheData('tom');
知道了它们的核心区别,就知道了类型声明是比类型断言更加严格的。
所以为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的as语法更加优雅。
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();
我们还有第三种方式可以解决这个问题,那就是泛型:
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,是最优的一个解决方案。
