[toc]
类型断言
类型断言(Type Assertion): 主要用于当 TypeScript 推断出来类型并不满足当前需求时,TypeScript 允许开发者覆盖它的推断,可以用来手动指定一个值的类型。
类型断言是一个编译时语法,不涉及运行时。
语法
值 as 类型
**(推荐)**或
<类型>值
形如
<Foo>
的语法在 ts 中除了表示类型断言之外,也可能是表示一个泛型,故建议在使用类型断言时,使用
值 as 类型
语法。
类型断言的用途
类型断言的常见用途有以下几种:
联合类型可以被断言为其中一个类型
上一篇文章中介绍访问联合类型的属性和方法,当 TS 不确定一个联合类型的变量到底是哪个类型时,只能访问此联合类型的所有类型中共有的属性或方法:
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;}// Property \'swim\' does not exist on type \'Cat | Fish\'.// Property \'swim\' does not exist on type \'Cat\'.
上述报错可使用类型断言,将
animal
断言成
Fish
,就可以解决访问
animal.swim
报错的问题。
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;}return false;}
注意:类型断言只能够欺骗 TS 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误:
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
直接断言为
Fish
,隐藏了
animal
可能为
Cat
的情况,而 TS 编译器信任了我们的断言,故在调用
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;}return false;}
声明了函数
isApiError
,用来判断传入的参数是不是
ApiError
类型,其参数的类型肯定得是父类
Error
,因此该函数便可接受
Error
或它的子类作为参数。
但由于父类
Error
中没有
code
属性,故直接获取
error.code
会报错,需要使用类型断言获取
(error as ApiError).code
。
此案例中有一个更合适的方式来判断是不是
ApiError
,那就是使用
instanceof
, 因为
ApiError
是一个 JavaScript 的类,能够通过
instanceof
来判断
error
是否是它的实例:
class ApiError extends Error {code: number = 0;}class HttpError extends Error {statusCode: number = 200;}function isApiError(error: Error) {if (error instanceof ApiError) {return true;}return false;}
但有些情况下
ApiError
和
HttpError
不是一个真正的类,而只是一个 TS 的接口(
interface
),接口是一个类型,不是一个真正的值,它在编译结果中会被删除,就无法使用
instanceof
来做运行时判断了:
interface ApiError extends Error {code: number;}interface HttpError extends Error {statusCode: number;}function isApiError(error: Error) {if (error instanceof ApiError) {return true;}return false;}// \'ApiError\' only refers to a type, but is being used as a value here.
此时就只能用类型断言,通过判断是否存在
code
属性,来判断传入的参数是不是
ApiError
:
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
理想情况下,每个值的类型都具体而精确,但引用一个该类型上不存在的属性或方法,就会报错:
const foo: number = 1;foo.length = 1;// 数字类型上是没有 `length` 属性// Property \'length\' does not exist on type \'number\'.
而有时,非常确定一段代码不会出错,如给
window
上添加一个属性
foo
,但 TS 编译时会报错,提示
window
上不存在
foo
属性。:
window.foo = 1;// Property \'foo\' does not exist on type \'Window & typeof globalThis\'.
此时可以使用
as any
临时将
window
断言为
any
类型,在
any
类型的变量上,访问任何属性都是允许的。
(window as any).foo = 1;
将一个变量断言为
any
可以说是解决 TypeScript 中类型问题的最后一个手段。**它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用
as any
。**总之,一方面不能滥用
as 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
。
interface Animal {name: string;}interface Cat {name: string;run(): void;}let tom: Cat = {name: \'Tom\',run: () => { console.log(\'run\') }};let animal: Animal = tom;
Cat
包含了
Animal
中的所有属性,TypeScript 只关注最终的结构有什么关系——所以同
Cat extends Animal
是等价的:
interface Animal {name: string;}interface Cat extends Animal {run(): void;}
这也是为什么
Cat
类型的
tom
可以赋值给
Animal
类型的
animal
。
当
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
即可,这也是为了在类型断言时的安全考虑,毕竟毫无根据的断言是非常危险的。
综上所述:
- 联合类型可以被断言为其中一个类型
- 父类可以被断言为子类
- 任何类型都可以被断言为 any
- any 可以被断言为任何类型
- 要使得
A
能够被断言为
B
,只需要
A
兼容
B
或
B
兼容
A
即可
其实前四种情况都是最后一个的特例。
双重断言
既然:
- 任何类型都可以被断言为 any
- any 可以被断言为任何类型
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即可
的限制,可以使用双重断言
as any as Foo
,
将任何一个类型断言为任何另一个类型
。
但是若使用了双重断言,那么很可能会导致运行时错误。除非迫不得已,千万别用双重断言。
类型断言 vs 类型转换
类型断言不是类型转换,它不会真的影响到变量的类型。
类型断言只会影响编译时的类型,断言语句在编译结果中会被删除:
function toBoolean(something: any): boolean {return something as boolean;}toBoolean(1);// 返回值为 1
若要进行类型转换,还是需要调用类型转换的方法:
function toBoolean(something: any): boolean {return Boolean(something);}toBoolean(1);// 返回值为 true