逆變與協(xié)變是泛型類型中的一個概念,當(dāng)然不只只是 TS 獨有的概念。簡單來說,假設(shè)存在類型 T2 為 T1 的子類,并且從 T1 派生出新類型 N<T1>以及從 T2 中派生出新類型 N<T2>。如果可以將 N<T2> 的實例賦值給類型為 N<T1> 的實例,則稱為協(xié)變。如果能將 N<T1> 的實例賦值給類型 N<T2> 的實例則稱之為逆變。當(dāng)然如果兩種類型都不能賦值,則稱為不變。例如在 java 中集合類型為不變,而數(shù)組類型則支持協(xié)變。例如:
class People{
public string getName();
}
class Student extends People{
public string getId();
}
void SayName(People[] manyPeople){
for(People p: manyPeople){
p.sayName();
}
}
SayName(new Student[]); // 支持協(xié)變
void SayName(List<People>){
for(People p: manyPeople){
p.sayName();
}
}
SayName(new List<Student>()); // 不支持
類型兼容性
當(dāng)然在 TS 中類型之間不一定存在繼承關(guān)系,因此當(dāng) TS 判斷是否為結(jié)構(gòu)化子類型時有一個規(guī)則,如果實例 b 能夠賦值給實例 a,則 a 中的全部屬性需要能在 b 中找到,例如:
type People = {
name: string
}
type Student = {
name: string,
id: number
}
const s: Student = {name: 'lily', id: 5}
const p: People = s // People 的所有屬性能在 Student 中找到,可以賦值
const s1: Array<Student> = [s]
const p1: Array<People> = s1 // 支持協(xié)變
當(dāng)然在 TS 中還存在非結(jié)構(gòu)化類型的情況,例如:
type ManyName = 'lily'|'bob'|'john'
type Lily = 'lily'
const lily: Lily = 'lily'
const names: ManyName = lily
對于 Lily 可以復(fù)制給 ManyName 這個聯(lián)合類型,因此我們可以將 Lily 視為 ManyName 這個子類型。從結(jié)構(gòu)化類型來看,子類的屬性多于父類型。為什么這里的 ManyName 反而是父類型呢。其實無論是否結(jié)構(gòu)化,都可以認為父類型所描述的類型范圍更加廣泛,而子類型描述的范圍更加狹窄,從父類到子類其實是 narrowing 的過程。
逆變
大多數(shù)類型兼容都采用協(xié)變,但是涉及到函數(shù)時會不一樣。例如:
type People = {
name: string
}
type Student = {
name: string,
id: number
}
function say(p: People, sayWhat: (p: People) => void){
sayWhat(p)
}
function sayId(s: Student){
console.info(s.id)
}
function sayPeople(s: People){
console.info(s.name)
}
say({name: 'lily'}, sayId)
如果我們采用協(xié)變的思想,將 (s: Student) => void 的 sayId 賦值給 (s: People) => void。則意為著在調(diào)用 sayId 時可以傳入類型為 People 的參數(shù),但是此時 sayId 明顯時需要更多屬性的 Student 類型的參數(shù)。因此當(dāng)比較兩個函數(shù)類型是否兼容時,函數(shù)參數(shù)為逆變。我們可以將 sayPeople 賦值給類型為 (s: Student) => void,因為這表示 sayPeople 所接受到的參數(shù)類型必定為 Student 及其子類型, 而 Student 及其子類型則一定包含 sayPeople 所需要的 name 屬性。
雙向協(xié)變
在 TS 中,默認是采用雙向協(xié)變進行兼容,如果將 A 類型的值賦給 B 類型。則只要 A 能協(xié)變?yōu)?B 或者 B 能協(xié)變?yōu)?A 即可。如果需要嚴格的逆變方式兼容,可以在 tsconfig.json 中聲明:
{
"compilerOptions": {
"strictFunctionTypes": true
},
}
TS 的解釋為,我們在聲明某個方法時可能會使用更廣泛的類型,但是實際使用的時候則會傳入更詳細的子類型。例如:
interface MyEvent{
type: string
}
interface MyMouseEvent extends MyEvent{
x: number,
y: number
}
declare function on(eventName: string, callBack: (event: MyEvent) => void): void
on('mouse', (event: MyMouseEvent) => console.info(`${event.x} : ${event.y}`))
也就是說雖然聲明了需要一個 (event: MyEvent) => void 類型的回調(diào),但是當(dāng)調(diào)用這個回調(diào)時,依舊會傳入 MyMouseEvent 類型的 event。這在 js 中是非常常見的模式,也一般不會有什么錯誤。如果只支持逆變,那么可能會有大量的樣本代碼:
on('mouse', (event: Event) => {
const e = (event as unknown) as MyMouseEvent // 先轉(zhuǎn)換成實際傳入的類型
console.info(`${e.x} : ${e.y}`)
})
函數(shù)兼容性還有另一種情況:
type sayName = (name: string) => void
type sayNameAndId = (name: string, id: number) => void
let sn: sayName;
let sni: sayNameAndId;
sn = sni; // 不支持
sni = sn; // 支持
其實也可以看作為逆變,即可以將類型更廣泛的 sn 賦值給類型更狹窄的 sni。但是反過來卻不支持。其實這種模式在 js 中也很常用,例如 array 的 forEach 方法本身定義為:
forEach((element, index, array) => { /* ... */ })
當(dāng)然也可以傳遞只有一個參數(shù)的函數(shù)如:
forEach(x => console.info(x))
逆變協(xié)變對 infer 的影響
簡單一句話來說就是 infer 處于逆變位置推斷類型為交叉類型,處于協(xié)變位置推斷出類型為聯(lián)合類型:
interface Foo{
bar1(name: string): string,
bar2(name: number): number
}
type BAR<T> = T extends {
bar1: (name: infer R) => infer Y,
bar2: (name: infer R) => infer Y,
} ? [R, Y] : never
type BBB = BAR<Foo> // [never, string|number]. 第一個 never 是因為 string & number = never
這個只是還對應(yīng)一個非常經(jīng)典的一道題:
https://github.com/type-challenges/type-challenges/blob/master/questions/55-hard-union-to-intersection/README.md
type UnionToIntersection<U> =
(U extends unknown ? (arg: U) => unknown : never) extends ((arg: infer P) => unknown) ? P : never;