# 類型斷言
類型斷言(Type Assertion)可以用來手動指定一個值的類型。
## 語法
```ts
值 as 類型
```
或
```ts
<類型>值
```
在 tsx 語法(React 的 jsx 語法的 ts 版)中必須使用前者,即 `值 as 類型`。
形如 `<Foo>` 的語法在 tsx 中表示的是一個 `ReactNode`,在 ts 中除了表示類型斷言之外,也可能是表示一個[范型][]。
故建議大家在使用類型斷言時,統一使用 `值 as 類型` 這樣的語法,本書中也會貫徹這一思想。
## 類型斷言的用途
類型斷言的常見用途有以下幾種:
### 將一個聯合類型斷言為其中一個類型
[之前提到過](union-types.md#訪問聯合類型的屬性或方法),當 TypeScript 不確定一個聯合類型的變量到底是哪個類型的時候,我們**只能訪問此聯合類型的所有類型中共有的屬性或方法**:
```ts
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function getName(animal: Cat | Fish) {
return animal.name;
}
```
而有時候,我們確實需要在還不確定類型的時候就訪問其中一個類型特有的屬性或方法,比如:
```ts
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;
}
// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
// Property 'swim' does not exist on type 'Cat'.
```
上面的例子中,獲取 `animal.swim` 的時候會報錯。
此時可以使用類型斷言,將 `animal` 斷言成 `Fish`:
```ts
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;
}
```
這樣就可以解決訪問 `animal.swim` 時報錯的問題了。
需要注意的是,類型斷言只能夠「欺騙」TypeScript 編譯器,無法避免運行時的錯誤,反而濫用類型斷言可能會導致運行時錯誤:
```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`
```
上面的例子編譯時不會報錯,但在運行時會報錯:
```text
Uncaught TypeError: animal.swim is not a function`
```
原因是 `(animal as Fish).swim()` 這段代碼隱藏了 `animal` 可能為 `Cat` 的情況,將 `animal` 直接斷言為 `Fish` 了,而 TypeScript 編譯器信任了我們的斷言,故在調用 `swim()` 時沒有編譯錯誤。
可是 `swim` 函數接受的參數是 `Cat | Fish`,一旦傳入的參數是 `Cat` 類型的變量,由于 `Cat` 上沒有 `swim` 方法,就會導致運行時錯誤了。
總之,使用類型斷言時一定要格外小心,盡量避免斷言后調用方法或引用深層屬性,以減少不必要的運行時錯誤。
### 將一個父類斷言為更加具體的子類
當類之間有繼承關系時,類型斷言也是很常見的:
```ts
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`:
```ts
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;
}
```
上面的例子中,確實使用 `instanceof` 更加合適,因為 `ApiError` 是一個 JavaScript 的類,能夠通過 `instanceof` 來判斷 `error` 是否是它的實例。
但是有的情況下 `ApiError` 和 `HttpError` 不是一個真正的類,而只是一個 TypeScript 的接口(`interface`),接口是一個類型,不是一個真正的值,它在編譯結果中會被刪除,當然就無法使用 `instanceof` 來做運行時判斷了:
```ts
interface ApiError extends Error {
code: number;
}
interface HttpError extends Error {
statusCode: number;
}
function isApiError(error: Error) {
if (error instanceof ApiError) {
return true;
}
return false;
}
// index.ts:9:26 - error TS2693: 'ApiError' only refers to a type, but is being used as a value here.
```
此時就只能用類型斷言,通過判斷是否存在 `code` 屬性,來判斷傳入的參數是不是 `ApiError` 了:
```ts
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`
理想情況下,TypeScript 的類型系統運轉良好,每個值的類型都具體而精確。
當我們引用一個在此類型上不存在的屬性或方法時,就會報錯:
```ts
const foo: number = 1;
foo.length = 1;
// index.ts:2:5 - error TS2339: Property 'length' does not exist on type 'number'.
```
上面的例子中,數字類型的變量 `foo` 上是沒有 `length` 屬性的,故 TypeScript 給出了相應的錯誤提示。
這種錯誤提示顯然是非常有用的。
但有的時候,我們非常確定這段代碼不會出錯,比如下面這個例子:
```ts
window.foo = 1;
// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.
```
上面的例子中,我們需要將 `window` 上添加一個屬性 `foo`,但 TypeScript 編譯時會報錯,提示我們 `window` 上不存在 `foo` 屬性。
此時我們可以使用 `as any` 臨時將 `window` 斷言為 `any` 類型:
```ts
(window as any).foo = 1;
```
在 `any` 類型的變量上,訪問任何屬性都是允許的。
需要注意的是,將一個變量斷言為 `any` 可以說是解決 TypeScript 中類型問題的最后一個手段。
**它極有可能掩蓋了真正的類型錯誤,所以如果不是非常確定,就不要使用 `as any`。**
上面的例子中,我們也可以通過[擴展 window 的類型(TODO)][]解決這個錯誤,不過如果只是臨時的增加 `foo` 屬性,`as any` 會更加方便。
總之,**一方面不能濫用 `as any`,另一方面也不要完全否定它的作用,我們需要在類型的嚴格性和開發的便利性之間掌握平衡**(這也是 [TypeScript 的設計理念][]之一),才能發揮出 TypeScript 最大的價值。
### 將 `any` 斷言為一個具體的類型
在日常的開發中,我們不可避免的需要處理 `any` 類型的變量,它們可能是由于第三方庫未能定義好自己的類型,也有可能是歷史遺留的或其他人編寫的爛代碼,還可能是受到 TypeScript 類型系統的限制而無法精確定義類型的場景。
遇到 `any` 類型的變量時,我們可以選擇無視它,任由它滋生更多的 `any`。
我們也可以選擇改進它,通過類型斷言及時的把 `any` 斷言為精確的類型,亡羊補牢,使我們的代碼向著高可維護性的目標發展。
舉例來說,歷史遺留的代碼中有個 `getCacheData`,它的返回值是 `any`:
```ts
function getCacheData(key: string): any {
return (window as any).cache[key];
}
```
那么我們在使用它時,最好能夠將調用了它之后的返回值斷言成一個精確的類型,這樣就方便了后續的操作:
```ts
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` 的訪問時就有了代碼補全,提高了代碼的可維護性。
## 類型斷言的限制
> 本小結的前置知識點:[結構類型系統(TODO)][]、[類型兼容性(TODO)][]
從上面的例子中,我們可以總結出:
- 聯合類型可以被斷言為其中一個類型
- 父類可以被斷言為子類
- 任何類型都可以被斷言為 any
- any 可以被斷言為任何類型
那么類型斷言有沒有什么限制呢?是不是任何一個類型都可以被斷言為任何另一個類型呢?
答案是否定的——并不是任何一個類型都可以被斷言為任何另一個類型。
具體來說,若 `A` 兼容 `B`,那么 `A` 能夠被斷言為 `B`,`B` 也能被斷言為 `A`。
下面我們通過一個簡化的例子,來理解類型斷言的限制:
```ts
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` 中的所有屬性,除此之外,它還有一個額外的方法 `run`。TypeScript 并不關心 `Cat` 和 `Animal` 之間定義時是什么關系,而只會看它們最終的結構有什么關系——所以它與 `Cat extends Animal` 是等價的:
```ts
interface Animal {
name: string;
}
interface Cat extends Animal {
run(): void;
}
```
那么也不難理解為什么 `Cat` 類型的 `tom` 可以賦值給 `Animal` 類型的 `animal` 了——就像面向對象編程中我們可以將子類的實例賦值給類型為父類的變量。
我們把它換成 TypeScript 中更專業的說法,即:`Animal` 兼容 `Cat`。
當 `Animal` 兼容 `Cat` 時,它們就可以互相進行類型斷言了:
```ts
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 在判斷類型的兼容性時,比這種情況復雜很多,詳細請參考[類型的兼容性(TODO)][]章節。
總之,若 `A` 兼容 `B`,那么 `A` 能夠被斷言為 `B`,`B` 也能被斷言為 `A`。
同理,若 `B` 兼容 `A`,那么 `A` 能夠被斷言為 `B`,`B` 也能被斷言為 `A`。
所以這也可以換一種說法:
要使得 `A` 能夠被斷言為 `B`,只需要 `A` 兼容 `B` 或 `B` 兼容 `A` 即可,這也是為了在類型斷言時的安全考慮,畢竟毫無根據的斷言是非常危險的。
綜上所述:
- 聯合類型可以被斷言為其中一個類型
- 父類可以被斷言為子類
- 任何類型都可以被斷言為 any
- any 可以被斷言為任何類型
- 要使得 `A` 能夠被斷言為 `B`,只需要 `A` 兼容 `B` 或 `B` 兼容 `A` 即可
其實前四種情況都是最后一個的特例。
## 雙重斷言
既然:
- 任何類型都可以被斷言為 any
- any 可以被斷言為任何類型
那么我們是不是可以使用雙重斷言 `as any as Foo` 來將任何一個類型斷言為任何另一個類型呢?
```ts
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 編譯時的類型,類型斷言語句在編譯結果中會被刪除:
```ts
function toBoolean(something: any): boolean {
return something as boolean;
}
toBoolean(1);
// 返回值為 1
```
在上面的例子中,將 `something` 斷言為 `boolean` 雖然可以通過編譯,但是并沒有什么用,代碼在編譯后會變成:
```js
function toBoolean(something) {
return something;
}
toBoolean(1);
// 返回值為 1
```
所以類型斷言不是類型轉換,它不會真的影響到變量的類型。
若要進行類型轉換,需要直接調用類型轉換的方法:
```ts
function toBoolean(something: any): boolean {
return Boolean(something);
}
toBoolean(1);
// 返回值為 true
```
## 類型斷言 vs 類型聲明
在這個例子中:
```ts
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 Cat` 將 `any` 類型斷言為了 `Cat` 類型。
但實際上還有其他方式可以解決這個問題:
```ts
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` 類型。
它們的區別,可以通過這個例子來理解:
```ts
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` 類型:
```ts
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
const animal: Animal = {
name: 'tom'
};
let tom: Cat = animal;
// index.ts:12:5 - error TS2741: 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` 才行
但是 `Cat` 并不兼容 `Animal`。
而在前一個例子中,由于 `getCacheData('tom')` 是 `any` 類型,`any` 兼容 `Cat`,`Cat` 也兼容 `any`,故
```ts
const tom = getCacheData('tom') as Cat;
```
等價于
```ts
const tom: Cat = getCacheData('tom');
```
知道了它們的核心區別,就知道了類型聲明是比類型斷言更加嚴格的。
所以為了增加代碼的質量,我們最好優先使用類型聲明,這也比類型斷言的 `as` 語法更加優雅。
## 類型斷言 vs 范型
> 本小結的前置知識點:[范型][]
還是這個例子:
```ts
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();
```
我們還有第三種方式可以解決這個問題,那就是范型:
```ts
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`,是最優的一個解決方案。
## 參考
- [TypeScript Deep Dive / Type Assertion](https://basarat.gitbooks.io/typescript/content/docs/types/type-assertion.html)
- [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)))
- [TypeScript 的設計理念][]
---
- [上一章:函數的類型](type-of-function.md)
- [下一章:聲明文件](declaration-files.md)
[TypeScript 的設計理念]: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
[范型]: ../advanced/generics.md