类型断言
类型断言(TyPe Assertion)可以用来手动只顶一个值的类型。
语法
ts
值 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
}而有时候 我们需要访问在还不确认类型的时候访问其中一个类型特有的方法和属性,比如
ts
function isFish(animal:Cat | Fish ){
if(typeof animal.swim === 'function'){
return true
}
return false
} // 类型"Cat | Fish"上不存在属性"swim"。类型"Cat"上不存在属性"swim"。
```ts
上例中,获取`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)
}
ts
这样的设计其实也很容易就能理解:
- 允许`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`肯定会报错,因为i`Cat`和`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`函数添加了一个泛型<T>,我们可以更加规范的市县对应`getCacheData`返回值的约束,这也同时去除掉了代码中的 `any`,是最优的一个解决方案。