TypeScript 入門自學筆記 — 型別斷言(二)

Echoyya、發表於2021-04-28

碼文不易,轉載請帶上本文連結,感謝~ https://www.cnblogs.com/echoyya/p/14558034.html

型別斷言

型別斷言(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;
}

但有些情況下 ApiErrorHttpError 不是一個真正的類,而只是一個 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 能夠被斷言為 BB 也能被斷言為 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 相容 BB 相容 A 即可,這也是為了在型別斷言時的安全考慮,畢竟毫無根據的斷言是非常危險的。

綜上所述:

  • 聯合型別可以被斷言為其中一個型別
  • 父類可以被斷言為子類
  • 任何型別都可以被斷言為 any
  • any 可以被斷言為任何型別
  • 要使得 A 能夠被斷言為 B,只需要 A 相容 BB 相容 A 即可

其實前四種情況都是最後一個的特例。

雙重斷言

既然:

  • 任何型別都可以被斷言為 any
  • any 可以被斷言為任何型別
interface Cat {
    run(): void;
}
interface Fish {
    swim(): void;
}

function testCat(cat: Cat) {
    return (cat as any as Fish);
}

若直接使用 cat as Fish 肯定會報錯,因為 CatFish 互相都不相容。

但是使用雙重斷言,則可以打破要使得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 入門自學筆記(一)


相關文章