我也是最近剛接觸到了這些知識,文章可能有些錯誤,希望大佬多多指點(
對於學習 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">;
這裡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">;