类型断言
有时候,你会遇到这样的情况,你会比typeScript更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式可以告诉编辑器,"相信我,我知道自己在干什么"。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和结构。它没有运行时的影响,只在编译阶段起作用,typeScript会假设你,程序员,已经进行了必要的检查。
作用
类型断言可以用来手动指定一个值的类型、
语法
值 as 类型
或
<类型>值
值 as 类型
let someVale:any = "this is a string"
let strLength : number = (someValue as any).length
<类型>值
let someValue : any = 'this is string'
let strLength : number = (<string>someValue).length
注意,两种形式是等价的。至于使用哪个凭个人喜好;然后,当在typeScript中使用jsx语法时,只有as语法断言是被允许的。
将一个联合类型断言为其中一个类型
之前提到过,当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
}
// TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
// Property 'swim' does not exist on type 'Cat'.
上面例子中,获取animal.swim
的时候会报错
此时,可以使用类型断言,将animal
断言为Fish
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') {
return true;
}
// if(typeof (<Fish>animal).swim === 'function') 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 方法,就会导致运行时错误了。
总之,使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。
讲一个父类断言为更加具体的子类
class ApiError extends Error {
code:number=0;
}
class HttpError extends Error{
statusCode: number = 200
}
function isApiError(error:Error) {
// if (typeof (error as ApiError).code === 'number') {
// return true
// }
if (error instanceof ApiError) {
return true
}
return false
}
上面的例子中,我们声明了函数isApiError
,它用来判断传入的参数是不是ApiError
类型,为了实现这样一个函数,他的参数的类型肯定得是比较抽象的父类Error
,这样的话这个函数就能接受Error
或它的子类做为参数了。
但是由于父类Error
中没有code
属性,故直接获取error.code
会报错,需要使用类型断言获取(error as ApiError).code
大家可能注意到,在这个例子中有一个更合适的方式来判断是不是ApiError
,那就是使用instanceof
上面例子中,确实使用instanceof
更加合适,因为ApiError
是一个JavaScript的类,能够通过instanceof
来判断error
是否是它的实例。
但是有的情况下ApiError
和HttpError
不是一个真正的类,而只是一个TypeScript的接口(interface),接口是一个类型,不是一个真正的值,它在编译结果中会被删除,当然就无法使用instanceof
来做运行判断了。此时只能用类型断言,通过判断是否存在code属性,来判断传入的参数是不是ApiError
了。
将任何一个类型断言为any
理想情况下,TypeScript的类型系统运转良好,每个值的类型都具体而精确。
当我们引用一个在此类型上不存在的属性或方法时,就会报错:
const foo: number = 1
foo.length = 1
// - error TS2339: Property 'length' does not exist on type 'number'.
上面的例子中,数字类型的变量foo上是没有length属性的,故TypeScript给出了相应错误提示。
这种错误提示显示是非常共有用的。
但有的时候,我们非常确定这段代码不会出错,比如下面这个例子
window.foo = 1;
// - 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。
上面例子中,我们也可以通过扩展window的类型(TODO)[]解决这个错误,不过如果只是临时的增加foo属性,as any会更加方便。
总之,一方面不能滥用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
类型,后续对代码的访问时,就有了代码补全,提高了代码的可维护性。
类型断言的限制
从上面例子中,我们可以总结出
- 联合类型可以被断言为其中一个类型。
- 任何类型都可以被断言为any
- any可以被断言为任何类型
那么类型断言有没有什么限制呢?是不是任何一个类型都可以被断言为任何另一个类型呢?答案是否定的——并不是任何一个类型都可以断言为任何另一个类型。
具体来说,若A兼容B,那么A能够被断言为B,B也能被断言为A。
下面我们通过一个简化的例子,来理解类型断言的限制
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
let tom: Cat = {
name: 'Tom',
run: () => { console.log('run') }
};
let animal: Animal = tom;
我们知道,TypeScript是结构类型的系统,类型之间的对比只会比较它们最终的结构,而会忽略它们定义时的关系。
在上面例子中,Cat包含了Animal中的所有属性,楚辞之外,它还有一个额外的方法run。TypeScript并不关心Cat和Animal之间定义时是什么关系。而只会看他们最终的结构有什么关系——所以它与Cat extends Animal是等价的
interface Animal{
name: string
}
interface Cat extends Animal{
run(): void
}
那么也不难理解为什么Cat
类型的tom
可以赋值给Animal
了——就想面向对象编程中我们可以将子类的实例赋值为父类的变量。
我们把它转换成TypeScript中更专业的说法,即:Animal
兼容Cat
。
当Animal
兼容Cat
时,它们就可以互相进行类型断言了:
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);
}
这样设计其实也很容易理解:
- 允许
animal as Cat
是因为【父类可以断言为子类】,这前面已经学习过了。 - 允许
cat as Animal
是因为既然子类拥有父类的属性和方法,那么被断言为父类,获取父类的属性、调用父类的方法,就不会有任何问题,【故子类可以被断言为父类】。
总之,若A兼容B,那么A能够被断言为B,B能够被断言为A;
若B兼容A,那么A能够被断言为B,B也能够被断言为A。
所以这也可以换一种说法:
要使得A能够被断言为B,只需要A兼容B或者B兼容A即可,这也就是为了在类型断言时安全考虑,毕竟毫无根据的断言是非常危险的。
综上所述:
- 联合类型可以被断言为其中一个类型
- 父类型可以被断言为子类
- 任何类型都可以被断言为any
- any可以被的断言为任何类型
- 要使得A能够被断言为B,只需要A兼容B或者B兼容A即可
其实前四种情况都是最后一个的特例
双重断言
既然:
- 任何类型可以被断言为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即可】的限制,将任何一个类型断言为任何另一个类型。
若你使用了这种双重断言,那么十有八九是非常错误的,它很可能会导致运行时错误。
除非迫不得已,千万别用双重断言。
类型断言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
类型。
它们的区别可以通过这个例子来理解:
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
// - error TS2741: Property 'run' is missing in type 'Animal' but required in type 'Cat'.
则会报错,不允许animal赋值为Cat类型的tom
这很容易理解,Animal可以看做是Cat的父类,当然不能将父类的实例赋值给类型为子类的变量。
深入的将,他们的核心区别在于:
- 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
语法更加的优雅
类型断言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
,是最优的一个解决方案。