TypeScript基础篇 --- 类型断言

类型断言

类型断言(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是否是它的实例。
但是有的情况下ApiErrorHttpError不是一个真正的类,而只是一个 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 anywindow断言为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中的所有属性,除此之外,他还有一个额外的方法runTypeScript并不关心CatAnimal之间的定义时是什么关系,而只会看它们最终的结构有什么关系——————所以他与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肯定会报错,因为iCatFish互相都不兼容。
但是如果使用双重断言,则可以打破要使得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 Catany类型断言为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,是最优的一个解决方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值