TypeScript 入门自学笔记 — 类型断言(二)
[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
上一篇:TypeScript 入门自学笔记(一)
- Python基础自学实用笔记 (三):9、random库 10、数值类型 11、查看数据类型 12、运算符优先级 13、条件表达式(三元操作符) 14、断言(assert)15、for循环
- 【Python入门自学笔记专辑】——函数式编程
- Python编程:从入门到实践 学习笔记 基础知识(一) 变量与简单数据类型
- python3.5入门笔记(三)--------反射、断言、socket通信、socketserver单/多并发
- Android NDK JNI 入门笔记-day02-基本数据类型数据传递
- 《TypeScript 中文入门教程》 1、基础数据类型
- css入门自学笔记1
- c#经典入门学习笔记-结构类型与对象的比较
- Go入门自学宝典003-变量(基本数据类型)
- c++入门笔记(5)数据类型
- 【自学笔记】之LINUX入门的那些破事儿——awk入门
- 学习笔记(04):Python初级入门精讲-string类型API-2
- Delphi初浅入门笔记之六:高级数据类型
- Python入门笔记 之 基本数据类型和变量
- TypeScript基础入门 - 枚举 - 联合枚举与枚举成员的类型
- PYTHON 学习笔记1 PYTHON 入门 搭建环境与基本类型
- 自学笔记二:C#语法基础 数据类型之值类型
- 自学笔记三:C#语法基础 数据类型之引用类型
- 自学笔记:正则表达式入门
- 类型即正义:TypeScript 从入门到实践(序章)