类型断言
类型断言(TyPe Assertion)可以用来手动只顶一个值的类型。
语法
值 as 类型
或
<类型>值
在 tsx语法(react的jsx语法的ts版)中必须使用前者,即值 as 类型
。
形如<Foo>
的语法在tsx中表示一个ReactNode
,在ts中除了表示类型断言外,也可能表示一个泛型。
故建议使用类型断言的时候,统一使用值 as 类型
这样的语法。
类型断言的用途
将一个联合类型断言为其中一个类型
之前有提到过,当 TypeScript
不确定一个联合的变量到底是那个类型的时候,我们只能访问此联合类型的所有类型中共有的属性和方法:
interface Cat{
name:string,
run():void;
}
interface Fish{
name:string;
swim():void;
}
function getName(animal:Cat | Fish ){
return animal.name
}
而有时候 我们需要访问在还不确认类型的时候访问其中一个类型特有的方法和属性,比如
function isFish(animal:Cat | Fish ){
if(typeof animal.swim === 'function'){
return true
}
return false
} // 类型“Cat | Fish”上不存在属性“swim”。类型“Cat”上不存在属性“swim”。
上例中,获取animal.swin
的时候会报错。
此时可以是用类型断言,将animal
断言成Fish
:
function isFish(animal:Cat | Fish ){
if(typeof (animal as Fish).swim === 'function'){
return true
}
return false
}
这样就可以解决访问animal.swim
时报错的问题。
但是需要注意的时候,类型断言只能够[欺骗]TypeScript
编译器,无法避免运行时的错误,所以滥用类型断言可能会导致运行的错误。
function isFish(animal:Cat | Fish ){
(animal as Fish).swim()
}
如果传入的是一个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
}
return false
}
上面例子中,参数限定为一个Error
类型,这个函数就能接受Error
和他的子类作为参数,为了判断是否是ApiError
类型,通过code
属性进行判断,但是Error
中没有code
直接获取error.code
会报错,所以这里使用类型断言为他的子类获取(error as ApiError).code
。
大家可能会注意到,在这个例子中有一个更合适的方式来判断是不是ApiError
,那就是使用instanceof
:
function isApiError(error:Error){
if(error instanceof ApiError){
return true
}
return false
}
上面例子中,确实使用instanceof
更加合适,因为ApiError
是一个 JavaScript
的类,能够使用instanceof
来判断error
是否是它的实例。
但是有的情况下ApiError
和HttpError
不是一个真正的类,而只是一个 TypeScript
的接口,接口时一个类型,不是一个真正的值,它在编译结果中会被删除,当然无法使用instanceof
来运行判断了:
interface ApiError extends Error{
code:number = 0 // 接口函数不能具有初始化表达式。
}
interface HttpError extends Error{
statusCode:number
}
function isApiError(error:Error){
if(error instanceof ApiError){
return true
}
return false
} // “ApiError”仅表示类型,但在此处却作为值使用
此时就只能用类型断言,用过判断是否存在code
属性,来判断传入的参数是不是ApiError
。
将任何一个类型断言为any
理想情况下,TypeScript
的类型系统运转良好,每个值的类型都具体而准确。
当我们引用一个在此类型上不存在的属性或方法时,就会报错:
const foo:number = 1
foo.length = 1 //类型“number”上不存在属性“length”。
上面例子中,数字类型的变量foo
上时没有length
属性的,所以 TypeScript
会给出相对于的错误。
但是有时候,我们非常确定这段代码不会出错,比如下面这个例子:
window.foo=1 //类型“Window & typeof globalThis”上不存在属性“foo”。
上面例子中,我们需要将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
,另一方也不要完全否定它的作用,我们需要在类型的鄢严格性和开发的便利性之间掌握平衡
将any
断言为一个具体的类型
在日常开发中,不可避免的需要处理any
类型的变量,他们可能时由于第三方库未能定义好自己的类型,也可能历史遗留或其他人编写的烂代码,还可能时受到 TypeScript
类型系统的限制而无法精确定义类型的场景。
遇到any
类型的变量时,我们可以选择无视它,任由它滋生更多的any
.
我们也可以改进它,通过类型断言及时的把any
断言为更精确的类型,亡羊补牢,是我们的代码想着高可维护性的目标发展。
例如,历史遗留的代码阿忠又getCacheData
,它的返回值是any
:
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可以被断言为任何类型
那么类型断言有没有什么限制呢?是不是任意一个类型都可以断言为任何另一个类型呢?
答案是否定的—————并不是任何一个类型都可以断言为任何另一个类型。
具体来说,若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
类型的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
是因为 既然子类拥有父类的属性和方法,那么被断言为父类,获取父类的属性,调用父类的方法就不会有任何问题,故子类可以被断言为父类。
需要注意的是,这里我使用了简化的父类子类的关系来表达类型的兼容,而实际上 TypeScript
在判断类型的兼容性时,比这种情况复杂很多。
总之,如果A
兼容B
,那么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 {
name:string
run():void
}
interface Fish {
name:string
swim():void
}
function testCat(cat:Cat){
return (cat as any as Fish)
}
上面例子中,若直接使用cat as Fish
肯定会报错,因为iCat
和Fish
互相都不兼容。
但是如果使用双重断言,则可以打破要使得A能够断言为B,只需要A兼容B或B兼容A即可的限制,将任何一个类型断言为任意另一个类型。
若你使用了这种双重断言,那么十有八九时非常错误的,他可能导致运行时错误。
除非迫不得已,千万别使用双重断言。
类型断言 vs 类型转化
类型断言只会影响 TypeScript
编译时的类型,类型断言语句在编译结果中会被删除:
function toBoolead(something:any):boolean {
return something as boolean
}
toBoolead(1)
在上面的例子中,将something
断言为boolean
虽然可以通过编译,但是并没有什么用,代码在编译后会变成:
function toBoolead(something) {
return something;
}
toBoolead(1);//1
所以类型断言不是类型转化,它不会真的影响到变量的类型。
如果要进行类型转化,需要直接调用类型转化的方法:
function toBoolean(something:any):boolean{
return Boolean(something)
}
toBoolead(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 Cat{
name:string
run():void
}
const animal:Animal = {
name:'tom'
}
let tom1 = animal as Cat
let tom2:Cat = animal // 类型 "Animal" 中缺少属性 "run",但类型 "Cat" 中需要该属性。
在上面例子中,由于Animal
兼容Cat
,故可以将animal
断言为Cat
赋值给tom1
;
但是将tom2
声明为Cat
,因为Animal
中缺少Cat
的方法,尽管Animal
可以看作为Cat
的父类,不能将父类的实例赋值给子类的变量。
深入的讲,它们的核心区别就在于:
animal
断言为Cat
,只需要满足Animal
兼容Cat
或者Cat
兼容Animal
即可animal
赋值给tom
,需要满足Cat
兼容Animal
但是Cat
并不兼容Animal
,所以例子中,赋值报错了,而前一个例子中,getCacheData('tom')
是any
类型,Cat
兼容any
,所以可以赋值。
所以,类型声明是比类型断言更加严格的。
为了代码的质量,我们最好优先使用函数声明,这也是比类型断言as
更加优雅。
类型断言 vs 泛型
还是之前那个例子,我们还有第三种方式可以解决这个问题,那么就是泛型:
function getCacheData<T>(key:string):T{
return (window as any).cache[key]
}
interface Cat{
name:string
}
let tom = getCacheData<Cat>('tom')
通过给getCacheData
函数添加了一个泛型,我们可以更加规范的市县对应getCacheData
返回值的约束,这也同时去除掉了代码中的 any
,是最优的一个解决方案。