suemor

suemor

前端萌新
telegram
github
twitter

TypeScript 類型父子級、逆變、協變、雙向協變和不變

我也是最近剛接觸到了這些知識,文章可能有些錯誤,希望大佬多多指點(

對於學習 TypeScript 了解類型的逆變、協變、雙向協變和不變是很重要的,但你只要明白類型的父子級關係,這些概念理解起來就會容易許多,因此在講述這些之前我們必須先學會類型的父子級關係。

類型的父子級#

首先明確一個概念,對於 TypeScript 而言,只要類型結構上是一致的,那麼就可以確定父子關係,這點與 Java 是不一樣的(Java 必須通過 extends 才算繼承)。

我們可以看下面的例子:

interface Person {
    name: string;
    age: number;
} 

interface Suemor {
    name: string;
    age: number;
    hobbies: string[]
}

你應該可以發現這兩個類型是有繼承關係,此時你可以去思考到底誰是父級、誰是子級?

你可能會覺得 Suemor 是 Person 的父類型(畢竟 Person 有 2 個屬性,而 Suemor 有 3 個屬性且包含 Person),如果是這麼理解的話那就錯。

在類型系統中,屬性更多的類型是子類型,也就是說 Suemor 是 Person 的子類型

因為這是反直覺的,你可能很難理解(我當時也理解不了),你可以嘗試這樣去理解:因為 A extends B , 於是 A 就可以去擴展 B 的屬性,那麼 A 的屬性往往會比 B 更多,因此 A 就是子類型。或者你記住一個特徵,子類型比父類型更加具體

另外判斷聯合類型父子關係的時候, 'a' | 'b' 和 'a' | 'b' | 'c' 哪個更具體?

'a' | 'b' 更具體,所以 'a' | 'b' 是 'a' | 'b' | 'c' 的子類型。

協變#

對象中運用#

協變理解起來很簡單,你可能在平日裡開發經常用到,例如:

interface Person {
    name: string;
    age: number;
} 

interface Suemor {
    name: string;
    age: number;
    hobbies: string[]
}

let person: Person = { // 父級
    name: '',
    age: 20
};
let suemor: Suemor = { // 子級
    name: 'suemor',
    age: 20,
    hobbies: ['play game', 'codeing']
};

//正確
person = suemor;
//報錯,如果你的編輯器沒有報錯,請打開嚴格模式,至於為什麼後面雙向協變會講
suemor = person;

這兩類型不一樣,但是 suemor 卻可以賦值給 person,也就是子級可以賦值給父級,反之不行(至於為什麼,你可以想想假如 person 能夠正確賦值給 suemor,那麼調用 suemor.hobbies你的程序就壞掉了)。

因此得出結論: 子類型可以賦值給父類型的情況就叫做協變。

函數中運用#

同樣的函數中也可以用到協變,例如:

interface Person { 
  name: string;
  age: number;
}

function fn(person: Person) {} // 父級

const suemor = { // 子級
  name: "suemor",
  age: 19,
  hobbies: ["play game", "codeing"],
};

fn(suemor);

fn({
  name: "suemor",
  age: 19,
  // 報錯
  // 這裡補充個知識點(因為當時我學的時候腦抽了),這裡的 hobbies 會報錯,是因為它是直接賦值,並沒有類型推導。
  hobbies: ["play game", "codeing"] 
})

這裡我們多給一個 hobbies,同理因為協變,子類型可以賦值給父類型。

因此我們平日的redux,在聲明 dispatch 類型的時候,可以這樣去寫:

interface Action {
  type: string;
}

function dispatch<T extends Action>(action: T) {

}

dispatch({
  type: "suemor",
  text:'測試'
});

這樣約束了傳入的參數一定是 Action 的子類型,也就是說必須有 type,其他的屬性有沒有都可以。

雙向協變#

我們再看一下上上節的例子:

interface Person {
    name: string;
    age: number;
} 

interface Suemor {
    name: string;
    age: number;
    hobbies: string[]
}

let person: Person = { // 父級
    name: '',
    age: 20
};
let suemor: Suemor = { // 子級
    name: 'suemor',
    age: 20,
    hobbies: ['play game', 'codeing']
};

//正確
person = suemor;
//報錯 -> 設置雙向協變可以避免報錯
suemor = person;

suemor = person的報錯我們可以在 tsconfig.json設置 strictFunctionTypes:false或者關閉嚴格模式,此時我們父類型可以賦值給子類型,子類型可以賦值給父類型,這種情況我們便稱為雙向協變

因此雙向協變就是: 父類型可以賦值給子類型,子類型可以賦值給父類型

但是這明顯是有問題的,不能保證類型安全,因此我們一般都會打開嚴格模式,避免出現雙向協變。

不變#

不變是最簡單的。如果沒有繼承關係(A 和 B 沒有一方包含對方全部屬性)那它就是不變,因此非父子類型之間只要類型不一樣就會報錯:

interface Person {
  name: string;
  age: number;
}

interface Suemor {
  name: string;
  sex:boolean
}

let person: Person = {
  name: "",
  age: 20,
};

let suemor: Suemor = {
  name: 'suemor',
  sex:true
};

// 報錯
person = suemor;

逆變#

逆變相對難理解一點,看下方例子:

let fn1: (a: string, b: number) => void = (a, b) => {
  console.log(a);
};
let fn2: (a: string, b: number, c: boolean) => void = (a, b, c) => {
  console.log(c);
};

fn1 = fn2; // 報錯
fn2 = fn1; // 這樣可以

你會發現:fn1 的參數是 fn2 的參數的父類型,那為啥能賦值給子類型?

這就是逆變,父類型可以賦值給子類型,函數的參數有逆變的性質(而返回值是協變的,也就是子類型可以賦值給父類型)。

至於為什麼,如果fn1 = fn2是正確的話,我們只能傳入fn1('suemor',123),但 fn1調卻要輸出 c,那就壞掉了。

因此我感覺逆變一般會出現在:父函數參數與子函數參數之間賦值的時候(注意是函數與函數之間,而不是調用函數的時候,我是這麼理解的,不知道對不對)。

因為逆變相對在類型做運算時用的會多一點,因此我們再看一個稍微難一點例子:

// 提取返回值類型
type GetReturnType<Func extends Function> = Func extends (
  ...args: unknown[]
) => infer ReturnType
  ? ReturnType
  : never;

type ReturnTypeResullt = GetReturnType<(name: string) => "suemor">;

image-20230203205737963

這裡GetReturnType用來提取返回值類型,這裡ReturnTypeResullt原本應當是suemor,但如上代碼卻得出結果為never

因為函數參數遵循逆變,也就是只能父類型賦值給子類型,但很明顯這裡的 unknown{name: string} 的父類型,所以反了,應該把unknown改為string的子類型才行,所以應該把 unknown 改為any或者never,如下為正確答案:

type GetReturnType<Func extends Function> = Func extends (
  ...args: any[]
) => infer ReturnType
  ? ReturnType
  : never;

type ReturnTypeResullt = GetReturnType<(name: string) => "suemor">;

image-20230203205711934

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