Desmond

Desmond

An introvert who loves web programming, graphic design and guitar
github
bilibili
twitter

TypeScript 類型系統

結構化類型系統#

class Cat {
  meow() { }
  eat() { } // 去掉 eat 不會報錯
}

class Dog {
  eat() { }
}

function feedCat(cat: Cat) { }

// 報錯!
feedCat(new Dog())

這是因為,TypeScript 比較兩個類型並非通過類型的名稱(即 feedCat 函數只能通過 Cat 類型調用),而是比較這兩個類型上實際擁有的屬性與方法。也就是說,這裡實際上是比較 Cat 類型上的屬性是否都存在於 Dog 類型上。

在我們最初的例子裡,Cat 與 Dog 類型上的方法是一致的,所以它們雖然是兩個名字不同的類型,但仍然被視為結構一致,這就是結構化類型系統的特性。你可能聽過結構類型的別稱鴨子類型(Duck Typing),這個名字來源於鴨子測試(Duck Test)。其核心理念是,如果你看到一隻鳥走起來像鴨子,游泳像鴨子,叫得也像鴨子,那麼這隻鳥就是鴨子

class Cat {
  eat() { }
}

class Dog {
  bark() { }
  eat() { }
}

function feedCat(cat: Cat) { }

feedCat(new Dog())

這個時候為什麼卻沒有類型報錯了?這是因為,結構化類型系統認為 Dog 類型完全實現了 Cat 類型。至於額外的方法 bark,可以認為是 Dog 類型繼承 Cat 類型後添加的新方法,即此時 Dog 類可以被認為是 Cat 類的子類。同樣的,面向對象編程中的里氏替換原則也提到了鴨子測試:如果它看起來像鴨子,叫起來也像鴨子,但是卻需要電池才能工作,那麼你的抽象很可能出錯了

更進一步,在比較對象類型的屬性時,同樣會採用結構化類型系統進行判斷。而對結構中的函數類型(即方法)進行比較時,同樣存在類型的兼容性比較:

class Cat {
  eat(): boolean {
    return true
  }
}

class Dog {
  eat(): number {
    return 599;
  }
}

function feedCat(cat: Cat) { }

// 報錯!
feedCat(new Dog())

標稱類型系統#

標稱類型系統(Nominal Typing System)要求,兩個可兼容的類型,其名稱必須是完全一致的,比如以下代碼:

type USD = number;
type CNY = number;

const CNYCount: CNY = 200;
const USDCount: USD = 200;

function addCNY(source: CNY, input: CNY) {
  return source + input;
}

addCNY(CNYCount, USDCount)

在標稱類型系統中,CNY 與 USD 被認為是兩個完全不同的類型,因此能夠避免這一情況發生。在《編程與類型系統》一書中提到,類型的重要意義之一是限制了數據的可用操作與實際意義,這一點在標稱類型系統中的體現要更加明顯。比如,上面我們可以通過類型的結構,來讓結構化類型系統認為兩個類型具有父子類型關係,而對於標稱類型系統,父子類型關係只能通過顯式的繼承來實現,稱為標稱子類型(Nominal Subtyping)

在 TypeScript 中模擬標稱類型系統#

export declare class TagProtector<T extends string> {
  protected __tag__: T;
}

export type Nominal<T, U extends string> = T & TagProtector<U>;

在這裡我們使用 TagProtector 聲明了一個具有 protected 屬性的類,使用它來攜帶額外的信息,並和原本的類型合併到一起,就得到了 Nominal 工具類型。

export type CNY = Nominal<number, 'CNY'>;

export type USD = Nominal<number, 'USD'>;

const CNYCount = 100 as CNY;

const USDCount = 100 as USD;

function addCNY(source: CNY, input: CNY) {
  return (source + input) as CNY;
}

addCNY(CNYCount, CNYCount);

// 報錯了!
addCNY(CNYCount, USDCount);

類型層級#

image

判斷類型兼容性的方式#

對於變量 a = 變量 b,如果成立,意味著 <變量 b 的類型> extends <變量 a 的類型> 成立,即 b 類型是 a 類型的子類型

type Result = 'linbudu' extends string ? 1 : 2;
declare let source: string;

declare let anyType: any;
declare let neverType: never;

anyType = source;

// 不能將類型“string”分配給類型“never”。
neverType = source;

基礎類型與字面量類型#

type Result1 = "linbudu" extends string ? 1 : 2; // 1
type Result2 = 1 extends number ? 1 : 2; // 1
type Result3 = true extends boolean ? 1 : 2; // 1
type Result4 = { name: string } extends object ? 1 : 2; // 1
type Result5 = { name: 'linbudu' } extends object ? 1 : 2; // 1
type Result6 = [] extends object ? 1 : 2; // 1

很明顯,一個基礎類型和它們對應的字面量類型必定存在父子類型關係。嚴格來說,object 出現在這裡並不恰當,因為它實際上代表著所有非原始類型的類型,即數組、對象與函數類型,所以這裡 Result6 成立的原因即是[]這個字面量類型也可以被認為是 object 的字面量類型。我們將結論簡記為,字面量類型 < 對應的原始類型

聯合類型#

type Result7 = 1 extends 1 | 2 | 3 ? 1 : 2; // 1
type Result8 = 'lin' extends 'lin' | 'bu' | 'du' ? 1 : 2; // 1
type Result9 = true extends true | false ? 1 : 2; // 1
type Result10 = string extends string | false | number ? 1 : 2; // 1

字面量類型 < 包含此字面量類型的聯合類型,原始類型 < 包含此原始類型的聯合類型

type Result11 = 'lin' | 'bu' | 'budu' extends string ? 1 : 2; // 1
type Result12 = {} | (() => void) | [] extends object ? 1 : 2; // 1

同一基礎類型的字面量聯合類型 < 此基礎類型

合併一下結論,去掉比較特殊的情況,我們得到了這個最終結論:字面量類型 < 包含此字面量類型的聯合類型(同一基礎類型) < 對應的原始類型,即:

// 2
type Result13 = 'linbudu' extends 'linbudu' | '599'
  ? 'linbudu' | '599' extends string
    ? 2
    : 1
  : 0;

裝箱類型#

type Result14 = string extends String ? 1 : 2; // 1
type Result15 = String extends {} ? 1 : 2; // 1
type Result16 = {} extends object ? 1 : 2; // 1
type Result18 = object extends Object ? 1 : 2; // 1
type Result16 = {} extends object ? 1 : 2; // 1
type Result18 = object extends {} ? 1 : 2; // 1

type Result17 = object extends Object ? 1 : 2; // 1
type Result20 = Object extends object ? 1 : 2; // 1

type Result19 = Object extends {} ? 1 : 2; // 1
type Result21 = {} extends Object ? 1 : 2; // 1

原始類型 < 原始類型對應的裝箱類型 < Object 類型

Top Type#

type Result22 = Object extends any ? 1 : 2; // 1
type Result23 = Object extends unknown ? 1 : 2; // 1
type Result24 = any extends Object ? 1 : 2; // 1 | 2
type Result25 = unknown extends Object ? 1 : 2; // 2
type Result26 = any extends 'linbudu' ? 1 : 2; // 1 | 2
type Result27 = any extends string ? 1 : 2; // 1 | 2
type Result28 = any extends {} ? 1 : 2; // 1 | 2
type Result29 = any extends never ? 1 : 2; // 1 | 2

在 TypeScript 內部代碼的條件類型處理中,如果接受判斷的是 any,那麼會直接返回條件類型結果組成的聯合類型

type Result31 = any extends unknown ? 1 : 2;  // 1
type Result32 = unknown extends any ? 1 : 2;  // 1

Object < any / unknown

Bottom Type#

type Result33 = never extends 'linbudu' ? 1 : 2; // 1
type Result34 = undefined extends 'linbudu' ? 1 : 2; // 2
type Result35 = null extends 'linbudu' ? 1 : 2; // 2
type Result36 = void extends 'linbudu' ? 1 : 2; // 2

never < 字面量類型

其他比較場景#

  • 對於基類和派生類,通常情況下派生類會完全保留基類的結構,而不只是自己新增新的屬性與方法。在結構化類型的比較下,其類型自然會存在子類型關係。更不用說派生類本身就是 extends 基類得到的。
  • 對於聯合類型的類型層級比較,我們只需要比較一個聯合類型是否可被視為另一個聯合類型的子集,即這個聯合類型中所有成員在另一個聯合類型中都能找到
type Result36 = 1 | 2 | 3 extends 1 | 2 | 3 | 4 ? 1 : 2; // 1
type Result37 = 2 | 4 extends 1 | 2 | 3 | 4 ? 1 : 2; // 1
type Result38 = 1 | 2 | 5 extends 1 | 2 | 3 | 4 ? 1 : 2; // 2
type Result39 = 1 | 5 extends 1 | 2 | 3 | 4 ? 1 : 2; // 2
  • 數組和元組
type Result40 = [number, number] extends number[] ? 1 : 2; // 1
type Result41 = [number, string] extends number[] ? 1 : 2; // 2
type Result42 = [number, string] extends (number | string)[] ? 1 : 2; // 1
type Result43 = [] extends number[] ? 1 : 2; // 1
type Result44 = [] extends unknown[] ? 1 : 2; // 1
type Result45 = number[] extends (number | string)[] ? 1 : 2; // 1
type Result46 = any[] extends number[] ? 1 : 2; // 1
type Result47 = unknown[] extends number[] ? 1 : 2; // 2
type Result48 = never[] extends number[] ? 1 : 2; // 1

References:
https://juejin.cn/book/7086408430491172901

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。