Skip to content

Commit c819edb

Browse files
committed
更新类型断言
1 parent 954cc72 commit c819edb

File tree

1 file changed

+294
-37
lines changed

1 file changed

+294
-37
lines changed

basics/type-assertion.md

Lines changed: 294 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,92 +5,349 @@
55
## 语法
66

77
```ts
8-
<类型>
8+
as 类型
99
```
1010

1111
1212

1313
```ts
14-
as 类型
14+
<类型>
1515
```
1616

17-
在 tsx 语法(React 的 jsx 语法的 ts 版)中必须用后一种。
17+
在 tsx 语法(React 的 jsx 语法的 ts 版)中必须使用前者 `值 as 类型`
18+
19+
形如 `<Foo>` 的语法在 tsx 中表示的是一个 `ReactNode`,在 ts 中也可能是表示一个[范型][]
1820

19-
## 例子:将一个联合类型的变量指定为一个更加具体的类型
21+
故建议大家在使用类型断言时,统一使用 `值 as 类型` 这样的语法,本书中也会贯彻这一思想。
22+
23+
## 类型断言的用途
24+
25+
类型断言的常见用途有以下几种:
26+
27+
### 将一个联合类型的变量断言为其中一个类型
2028

2129
[之前提到过](union-types.md#访问联合类型的属性或方法),当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们**只能访问此联合类型的所有类型里共有的属性或方法**
2230

2331
```ts
24-
function getLength(something: string | number): number {
25-
return something.length;
32+
interface Cat {
33+
name: string;
34+
run(): void;
35+
}
36+
interface Fish {
37+
name: string;
38+
swim(): void;
2639
}
2740

28-
// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
29-
// Property 'length' does not exist on type 'number'.
41+
function getName(animal: Cat | Fish) {
42+
return animal.name;
43+
}
3044
```
3145

32-
而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型的属性或方法,比如:
46+
而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,比如:
3347

3448
```ts
35-
function getLength(something: string | number): number {
36-
if (something.length) {
37-
return something.length;
38-
} else {
39-
return something.toString().length;
49+
interface Cat {
50+
name: string;
51+
run(): void;
52+
}
53+
interface Fish {
54+
name: string;
55+
swim(): void;
56+
}
57+
58+
function isFish(animal: Cat | Fish) {
59+
if (typeof animal.swim === 'function') {
60+
return true;
4061
}
62+
return false;
4163
}
4264

43-
// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'string | number'.
44-
// Property 'length' does not exist on type 'number'.
45-
// index.ts(3,26): error TS2339: Property 'length' does not exist on type 'string | number'.
46-
// Property 'length' does not exist on type 'number'.
65+
// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
66+
// Property 'swim' does not exist on type 'Cat'.
4767
```
4868

49-
上例中,获取 `something.length` 的时候会报错。
69+
上例中,获取 `animal.swim` 的时候会报错。
5070

51-
此时可以使用类型断言,将 `something` 断言成 `string`
71+
此时可以使用类型断言,将 `animal` 断言成 `Fish`
5272

5373
```ts
54-
function getLength(something: string | number): number {
55-
if ((<string>something).length) {
56-
return (<string>something).length;
57-
} else {
58-
return something.toString().length;
74+
interface Cat {
75+
name: string;
76+
run(): void;
77+
}
78+
interface Fish {
79+
name: string;
80+
swim(): void;
81+
}
82+
83+
function isFish(animal: Cat | Fish) {
84+
if (typeof (animal as Fish).swim === 'function') {
85+
return true;
5986
}
87+
return false;
6088
}
6189
```
6290

63-
91+
这样就可以解决访问 `animal.swim` 时报错的问题了。
92+
93+
需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,甚至滥用类型断言可能会导致运行时错误:
6494

6595
```ts
66-
function getLength(something: string | number): number {
67-
if ((something as string).length) {
68-
return (something as string).length;
69-
} else {
70-
return something.toString().length;
96+
interface Cat {
97+
name: string;
98+
run(): void;
99+
}
100+
interface Fish {
101+
name: string;
102+
swim(): void;
103+
}
104+
105+
function swim(animal: Cat | Fish) {
106+
(animal as Fish).swim();
107+
}
108+
109+
const cat: Cat = {
110+
name: 'Tom',
111+
run() { console.log('run'); }
112+
};
113+
swim(cat);
114+
```
115+
116+
上面的例子编译时不会报错,但在运行时会报错:
117+
118+
```text
119+
Uncaught TypeError: animal.swim is not a function`
120+
```
121+
122+
原因是 `(animal as Fish).swim()` 这段代码隐藏了 `animal` 可能为 `Cat` 的情况,将 `animal` 直接断言为 `Fish` 了,TypeScript 编译器信任了我们的断言,故在调用 `swim()` 时没有编译错误。
123+
124+
可是 `swim` 函数接受的参数是 `Cat | Fish`,一旦传入的参数是 `Cat` 类型的变量,由于没有 `Cat` 上没有 `swim` 方法,就会导致运行时错误了。
125+
126+
总之,使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,减少不必要的运行时错误。
127+
128+
### 将一个父类断言为更加具体的子类的类型
129+
130+
当类之间有继承关系时,类型断言也是很常见的:
131+
132+
```ts
133+
class ApiError extends Error {
134+
code: number = 0;
135+
}
136+
class HttpError extends Error {
137+
statusCode: number = 200;
138+
}
139+
140+
function isApiError(error: Error) {
141+
if (typeof (error as ApiError).code === 'number') {
142+
return true;
143+
}
144+
return false;
145+
}
146+
```
147+
148+
上面的例子中,我们声明了函数 `isApiError`,它用来判断传入的参数是不是 `ApiError`,为了实现这样一个函数,它的参数的类型肯定需要是比较抽象的父类 `Error`,这样的话这个函数就能接受 `Error` 以及任何它的子类作为参数了。
149+
150+
但是由于父类 `Error` 中没有 `code` 属性,故直接获取 `error.code` 会报错,需要使用类型断言获取 `(error as ApiError).code`
151+
152+
大家可能会注意到,在这个例子中有一个更合适的方式来判断是不是 `ApiError`,那就是使用 `instanceof`
153+
154+
```ts
155+
class ApiError extends Error {
156+
code: number = 0;
157+
}
158+
class HttpError extends Error {
159+
statusCode: number = 200;
160+
}
161+
162+
function isApiError(error: Error) {
163+
if (error instanceof ApiError) {
164+
return true;
165+
}
166+
return false;
167+
}
168+
```
169+
170+
上面的例子中,确实使用 `instanceof` 更加合适,但是有的情况下 `ApiError``HttpError` 不是一个真正的类,而只是一个接口类型 `interface`,接口是一个类型,不是一个真正的值,它在编译结果中会被删除,当然就无法使用 `instanceof` 来做运行时判断了:
171+
172+
```ts
173+
interface ApiError extends Error {
174+
code: number;
175+
}
176+
interface HttpError extends Error {
177+
statusCode: number;
178+
}
179+
180+
function isApiError(error: Error) {
181+
if (error instanceof ApiError) {
182+
return true;
183+
}
184+
return false;
185+
}
186+
187+
// index.ts:9:26 - error TS2693: 'ApiError' only refers to a type, but is being used as a value here.
188+
```
189+
190+
此时就只能用类型断言,通过判断是否存在 `code` 属性,来判断传入的参数是不是 `ApiError` 了:
191+
192+
```ts
193+
interface ApiError extends Error {
194+
code: number;
195+
}
196+
interface HttpError extends Error {
197+
statusCode: number;
198+
}
199+
200+
function isApiError(error: Error) {
201+
if (typeof (error as ApiError).code === 'number') {
202+
return true;
71203
}
204+
return false;
72205
}
73206
```
74207

75-
类型断言的用法如上,在需要断言的变量前加上 `<Type>` 即可。
208+
### 将一个变量断言为 `any`
209+
210+
理想情况下,TypeScript 的类型系统运转良好,每个值的类型都具体而精确。
76211

77-
**类型断言不是类型转换,断言成一个联合类型中不存在的类型是不允许的**
212+
当我们引用一个在此类型上不存在的属性或方法时,就会报错
78213

79214
```ts
80-
function toBoolean(something: string | number): boolean {
81-
return <boolean>something;
215+
const foo: number = 1;
216+
foo.length = 1;
217+
218+
// index.ts:2:5 - error TS2339: Property 'length' does not exist on type 'number'.
219+
```
220+
221+
上例中,数字类型的变量 `foo` 上是没有 `length` 属性的,故 TypeScript 给出了相应的错误提示。
222+
223+
这种错误提示显然是非常有用的。
224+
225+
但有的时候,我们非常确定这段代码不会出错,比如下面这个例子:
226+
227+
```ts
228+
window.foo = 1;
229+
230+
// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.
231+
```
232+
233+
上例中,我们需要将 `window` 上添加一个 `foo` 变量,但 TypeScript 编译时会报错,提示我们 `window` 上不存在 `foo` 属性。
234+
235+
此时我们可以使用 `as any` 临时将 `window` 断言为 `any` 类型:
236+
237+
```ts
238+
(window as any).foo = 1;
239+
```
240+
241+
`any` 类型的变量上,访问任何属性都是允许的。
242+
243+
需要注意的是,将一个变量断言为 `any` 可以说是解决 TypeScript 中类型问题的最后一个手段。
244+
245+
**它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 `as any`**
246+
247+
上面的例子中,我们也可以通过[扩展 window 的类型][]解决这个错误,不过如果只是临时的增加 `foo` 属性,`as any` 会更加方便。
248+
249+
总之,**一方面不能滥用 `as any`,另一方面也不要完全否定它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡**(这也是 [TypeScript 的设计理念][]之一),才能发挥出 TypeScript 最大的价值。
250+
251+
### `any` 类型的变量断言为一个具体的类型
252+
253+
在日常的开发中,我们不可避免的需要处理 `any` 类型的变量,它们可能是由于第三方库未能定义好自己的类型,也有可能是历史遗留的或其他人编写的烂代码,还可能是受到 TypeScript 类型系统的限制而无法精确定义类型的场景。
254+
255+
遇到 `any` 类型的变量时,我们可以选择无视它,任由它滋生更多的 `any`
256+
257+
也可以选择改进它,通过类型断言及时的把 `any` 类型断言为精确的类型,亡羊补牢,使我们的代码向着高可维护性的目标发展。
258+
259+
比如说历史遗留的代码中有个 `getCacheData`,它的返回值是 `any`
260+
261+
```ts
262+
function getCacheData(key: string): any {
263+
return (window as any).cache[key];
82264
}
265+
```
266+
267+
那么我们在使用它时,最好能够将调用了它之后的返回值断言成一个精确的类型,这样就方便了后续的操作:
83268

84-
// index.ts(2,10): error TS2352: Type 'string | number' cannot be converted to type 'boolean'.
85-
// Type 'number' is not comparable to type 'boolean'.
269+
```ts
270+
function getCacheData(key: string): any {
271+
return (window as any).cache[key];
272+
}
273+
274+
interface Cat {
275+
name: string;
276+
run(): void;
277+
}
278+
279+
const tom = getCacheData('tom') as Cat;
280+
tom.run();
281+
```
282+
283+
上面的例子中,我们调用完 `getCacheData` 之后,立即将它断言为 `Cat` 类型。这样的话明确了 `tom` 的类型,后续对 `tom` 的访问时就有了代码补全,提高了代码的可维护性。
284+
285+
## 类型断言的范围
286+
287+
在上面的例子中,我们可以总结出:
288+
289+
- 联合类型可以被断言为子类型
290+
- 父类可以被断言为子类
291+
- 任何类型都可以被断言为 any
292+
- any 可以被断言为任何类型
293+
294+
但是,并不是任何类型都可以断言为任何类型:
295+
296+
To be continued...
297+
298+
## 类型断言 vs 类型转换
299+
300+
类型断言只会影响 TypeScript 编译时的类型,类型断言语句在编译结果中会被删除:
301+
302+
```ts
303+
function toBoolean(something: any): boolean {
304+
return something as boolean;
305+
}
306+
307+
toBoolean(1);
308+
// 返回值为 1
86309
```
87310

311+
在上面的例子中,将 `something` 断言为 `boolean` 虽然可以通过编译,但是并没有什么用,代码在编译后会变成:
312+
313+
```js
314+
function toBoolean(something) {
315+
return something;
316+
}
317+
318+
toBoolean(1);
319+
// 返回值为 1
320+
```
321+
322+
所以类型断言不是类型转换,它不会真的影响到变量的类型。
323+
324+
若要进行类型转换,需要直接调用类型转换的方法:
325+
326+
```ts
327+
function toBoolean(something: any): boolean {
328+
return Boolean(something);
329+
}
330+
331+
toBoolean(1);
332+
// 返回值为 true
333+
```
334+
335+
## 类型断言 vs 声明类型 vs 范型
336+
337+
> 本小节的前置知识点:[范型][]
338+
339+
To be continued...
340+
88341
## 参考
89342

90343
- [TypeScript Deep Dive / Type Assertion](https://basarat.gitbooks.io/typescript/content/docs/types/type-assertion.html)
91344
- [Advanced Types # Type Guards and Differentiating Types](http://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types)[中文版](https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/Advanced%20Types.html#类型保护与区分类型(type-guards-and-differentiating-types))
345+
- [TypeScript 的设计理念][]
92346

93347
---
94348

95349
- [上一章:函数的类型](type-of-function.md)
96350
- [下一章:声明文件](declaration-files.md)
351+
352+
[TypeScript 的设计理念]: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
353+
[范型]: ../advanced/generics.md

0 commit comments

Comments
 (0)