类型断言可以用来手动指定一个值的类型。
语法
值 as 类型
或者
<类型>值
类型断言的用途
将一个联合类型断言为其中一个类型
interface Cat {
name: string
run(): void
}
interface Fish {
name: stirng
swim(): void
}
function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') {
return true
}
return false
}
注意:使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行错误。
将一个父类断言为更加具体的子类
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
}
将任何一个类型断言为 any
(window as any).foo = 1
在 any
类型的变量上,访问任何属性都是允许的。
需要注意的是,将一个变量断言为 any
可以说是解决 TypeScript 中类型问题的最后一个手段。
它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要用 **as any**
。
总之,一方面不能滥用 **as any**
,另一方面也不要完全否定它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡(这也是TypeScript的设计理念之一),才能发挥出TypeScript的最大价值。
将 any
断言为一个具体的类型
遇到 any
类型的变量时,我们可以选择无视它,任由它滋生更多的 any
。
我们也可以选择改进它,通过类型断言及时的把 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
的访问时就有了代码补全,提高了代码的可维护性。
类型断言的限制
并不是任何一个类型都可以被断言为任何另一个类型
具体来说,若 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: any): boolean {
return Boolean(something)
}
toBoolean(1) // 返回值为true
类型断言 vs 类型声明
他们的区别可以通过这个例子来理解:
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: 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
才行
说明类型声明是比类型断言更加严格的。
所以为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的 as
语法更加优雅。
类型断言 vs 泛型
我们还有第三种方式可以解决这个问题,那就是泛型:
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
,是最优的一个解决方案。