逆變與協變---徹底弄懂TS相容性檢查

語言: CN / TW / HK

title: TS相容性 date: 2021-09-25 10:38:00 categories: TS筆記 tags: - TypeScript


TS相容性

本章的自己學TS 看官網看視訊 以及找資料 的筆記的一部分

這個TS相容性檢查 官網裡看上去一點點,實際上理解起來十分困難

記錄一下思路,本篇是TS相容性的合集

想看TS函式相容性的逆變與協變 可直接跳到 函式相容性 一節

要讀下去,首先要理解什麼的TS的相容性?

相容性的原則是Duck-Check,即

被賦值的變數的型別中的屬性 賦值源的型別 中都存在 型別檢查就會通過

介面的相容性

函式傳入的 變數型別 與 宣告型別 不匹配TS會進行相容性檢查

```ts interface Animal { name: string; age: number; }

interface Person { name: string; age: number; gender: number } // 要判斷目標型別Person是否能夠相容輸入的源型別Animal function getName(animal: Animal): string { return animal.name; }

let p = { name: 'lzy', age: 25, gender: 0 }

getName(p); // 只有在傳參的時候兩個變數之間才會進行相容性的比較, // 賦值的時候並不會比較,會直接報錯 let a: Animal = { name: 'lzy', age: 25, gender: 0 // 報錯 } ```

在第三篇文章 TS介面 中有寫道,

物件字面量 直接作為引數時 會經歷 額外屬性檢查

而改寫成 物件 再作為引數 傳入函式呼叫,則不會 檢查物件上的額外屬性,

很顯然,呼叫函式直接傳入 物件字面量,相當於是對形參(對變數)的直接賦值,而

對 變數賦值 不會進行相容性檢查,不容許額外屬性,直接報錯,

函式呼叫 傳參,對於 傳入一個 值為物件的變數 這種情況,卻是寬容的,允許相容

猜測: 具有額外屬性字面量作為物件 賦值給引數,再作為引數傳入函式呼叫,TS不報錯,

是因為TS認為在物件賦值的時候,已經做過變數檢查了,是安全的,所以允許相容.

基本型別的相容性

```ts //基本資料型別也有相容性判斷 let num : string|number; let str:string='zhufeng'; num = str;

//只要有toString()方法就可以賦給字串變數 let num2 : { toString():string }

let str2:string='jiagou'; num2 = str2; ```

類的相容性

類有 靜態部分 和 例項部分 的型別 在比較兩個變數的 類型別時,只有例項的成員會被比較

類的私有成員和受保護成員會影響相容性。 當檢查類例項的相容時,如果目標型別包含一個私有成員, 那麼源型別必須包含來自同一個類的這個私有成員。 同樣地,這條規則也適用於包含受保護成員例項的型別檢查。 這允許子類賦值給父類,但是不能賦值給其它有同樣型別的類。 ```ts class Animal { feet: number; // 如果下面的name前面加了public等,則 a = s會報錯,因為加了修飾符會被加入例項的一部分 constructor(name: string,numFeet: number) { } }

class Size { feet: number; // 如果下面的 numFeet 和 上面的 numFeet 都加了public,兩個等式依舊 不報錯 // 如果下面的 numFeet 和 上面的 numFeet 都加了private,兩個等式 都報錯 constructor(numFeet: number) { } }

let a: Animal; let s: Size;

a = s; // OK s = a; // OK ```

在類的例項部分比較時,只對比例項部分的結構而不在意型別名 ```ts class Animal{ name:string } class Bird extends Animal{ swing:number }

let a:Animal = new Bird();

//並不是java的那套 '父類相容子類,但子類不相容父類' //僅僅是因為bird 需要的屬性 swing:number, new Animal()沒有 let b:Bird = new Animal(); //報錯 下面舉例說明 與 型別是不是子類父類 無關,只要結構符合就行ts class Animal{ name:string } //如果父類和子類結構一樣,也可以的 class Bird extends Animal{}

let a:Animal = new Bird();

let b:Bird = new Animal(); // 兩個完全無關的類也一樣,可以相互賦值,只要結構符合 ```

函式的相容性

函式賦值給變數時 先比較函式的引數 再比較函式的返回值 ```ts type sumFunc = (a:number,b:number)=>number; let sum:sumFunc; function f1(a:number,b:number):number{ return a+b; } //可以省略一個引數 function f2(a:number):number{ return a; } sum = f2; //可以省略二個引數 function f3():number{ return 0; } //多一個引數可不行 function f4(a:number,b:number,c:number){ return a+b+c; } sum = f1; sum = f2; sum = f3; sum = f4; // 報錯

// 返回值的物件 可以多屬性,不能比變數要求的返回值少屬性 ``` 給變數賦值時 賦值的函式 的引數要求 只可比變數要求函式引數 少 給變數賦值時 賦值的函式 的返回值要求 可比變數要求函式引數 多

函式加上 引數型別的父子類的相容

在給限定了 函式型別的變數 賦值函式時

賦值給 變數的 函式

其引數 可以是 比變數型別定義裡 要求的屬性 更少(逆變)

返回值 可以是 比變數型別定義裡 要求的屬性 更多(協變)

簡化版:

給變數賦值的函式 引數可以屬性更少,返回值可以屬性更多

理解:

用於賦值的函式的 引數 的屬性 必須 比 被賦值的變數的要求 更少

用於賦值的函式的 返回值 的屬性 必須 比 被賦值的變數的要求 更多 為什麼這麼要求?

因為在實際應用中, 在型別檢查時,只檢查 變數的型別,而不是 檢查 變數被賦的值的型別(因為值是可變的,關鍵是值在變數賦值時已被檢查過)

如果不要求 用於賦值的函式的 返回值 的屬性 只能等於或更多

變數的 後續使用時 會出現,

型別檢查時,允許 使用返回值的某屬性(因為,變數型別約束裡寫了有啊),

編譯執行時,發現返回值的這個屬性undefined(因為,變數的真實函式值返回值比變數類約束的值少啊!!)

而執行報錯。

如果不要求 用於賦值的函式的 引數 的屬性 只能等於或更少

變數的 後續使用時 則會出現

型別檢查時,允許 使用 變數A 進行變數呼叫(因為,變數型別約束裡 變數對引數的屬性要求 A這個引數都有啊)

編譯執行時,發現 變數A 少了某個屬性(因為,變數的真實函式值 需要的引數屬性 確實 比變數型別約束裡多啊!!)

而執行報錯

```ts class Animal{} class Dog extends Animal{ public name:string = 'Dog' } class BlackDog extends Dog { public age: number = 10 } class WhiteDog extends Dog { public home: string = '北京' } let animal: Animal; let blackDog: BlackDog; let whiteDog: WhiteDog; // 報錯,:引數dog 與 引數blackDog不相容, // blackDog引數限定的函式需要age屬性,dog引數限定的變數 不強制要求, // 假設此處不報錯 // 到時候通過 childToChild變數,呼叫函式傳參可以是dog型別 // 但其函式值卻需要blackDog型別的age屬性,執行時可能報錯undefined const childToChild: (dog: Dog)=>Dog = (blackDog: BlackDog): BlackDog => { // 同時此處 返回值 換成了具有更多屬性的 blackDog,此處返回值沒有問題。 return new BlackDog() }

// 報錯:引數dog 與 引數blackDog不相容,blackDog需要age,dog不需要。 const childToParent: (dog: Dog)=>Dog = (BlackDog: BlackDog): Animal => { // 報錯:同時此處 返回值 換成了具有更少屬性的 blackDog,此處返回值也錯了。 return new Animal() }

// 引數是 需要屬性更少的aniamal,此處引數沒有問題 const parentToParent: (dog: Dog)=>Dog = (animal: Animal): Animal => { // 報錯:返回值不能是屬性更少的animal型別,只能是屬性更多的類 return new Animal() }

// 完全OK,用於賦值的函式 引數的型別屬性要求更少 返回值引數的型別屬性更多 const parentToChild: (dog: Dog)=>Dog = (animal: Animal): BlackDog => { return new BlackDog() } //(Animal → Greyhound) ≼ (Dog → Dog) ```

泛型的相容性

首先明確,介面就是一種型別 一種type,泛型得先轉換成具體type再進行判斷

原則:先根據 傳入的泛型T確定其具體的型別,再進行相容性判斷 ```ts //1.介面內容為空 或者 沒用到泛型 或者 傳入的T一樣 // 或者 y的T是x的T的子類時,是相等的 interface Empty{ name:string } let x!:Empty; let y!:Empty; x = y;

//2.介面內容不為空的時候不相等的 interface NotEmpty{ data:T } let x1!:NotEmpty; // !非空斷言 let y1!:NotEmpty; x1 = y1; // 報錯

//demo2中就相當於是下面這樣 interface NotEmptyString{ data:string }

interface NotEmptyNumber{ data:number } let xx2!:NotEmptyString; let yy2!:NotEmptyNumber; xx2 = yy2; // 報錯 ```

如果是沒有指定型別的泛型,則視為T為any ```ts let identity = function(x: T): T { }

let reverse = function(y: U): U { }

identity = reverse; // OK, because (x: any) => any matches (y: any) => any ```

列舉的相容性

列舉型別 與 數字型別 相互相容

不同列舉型別 不相容

```ts //數字可以賦給列舉 enum Colors {Red=1,Yellow=2} enum Colors2 {Red=1,Yellow=2} let c:Colors; let c2:Colors2; c = Colors.Red; c = 1; c = '1'; // 報錯 c = Colors2.Red // 報錯

//列舉值可以賦給數字 let n:number; n = 1; n = Colors.Red; ```

最後

菜鳥理解,可能會有一些錯誤,大佬手下留情,同時感謝指教。

卑微的推薦一波個人網站 羅紫宇的部落格,雖然其實也沒什麼東西