學過集合論的同窗必定知道子集的概念,使用ES6 class寫過繼承的同窗必定知道子類的概念,而使用過TypeScript的同窗,也許知道子類型的概念。git
可是你知道協變 (Covariant)、逆變 (Contravariant)、雙向協變 (Bivariant) 和不變 (Invariant) 這些概念嗎?你知道像TypeScript這種強大的靜態類型檢查的編程語言,是怎麼作類型兼容的嗎?咱們今天來聊聊。github
子類型是編程語言中一個有趣的概念,源自於數學中子集的概念:typescript
若是集合A的任意一個元素都是集合B的元素,那麼集合A稱爲集合B的子集。編程
而子類型則是面向對象設計語言裏常提到的一個概念,是繼承機制的一個產物,如下概念來源百度:數組
在編程語言理論中,子類型是一種類型多態的形式。這種形式下,子類型能夠替換另外一種相關的數據類型(超類型,英語:supertype)。安全
子類型與面嚮對象語言中(類或對象)的繼承是兩個概念。子類型反映了類型(即面向對象中的接口)之間的關係;而繼承反映了一類對象能夠從另外一類對象創造出來,是語言特性的實現。所以,子類型也稱接口繼承;繼承稱做實現繼承。編程語言
咱們能夠理解子類就是實現繼承,子類型就是接口繼承,下面這幅圖更精確的定義了這個概念,不少同窗應該知道這個例子:ide
這幅圖中,貓是一種動物,因此咱們說貓是動物的子集,貓是動物的子類,或者說貓這種類型是動物這種類型的子類型。函數
一下提到四個陌生的單詞,不少同窗確定一下就懵了。React開發者應該對HOC (High Order Component) 不陌生,它就是使用一個基礎組件做爲參數,返回一個高階組件的函數。React的基礎是組件 (Component),在TypeScript裏是類型 (Type),所以咱們用HOT (High Order Type) 來表示一個複雜類型,這個複雜類型接收一個泛型參數,返回一個複合類型。post
下面我用一個例子來闡述這四個概念,你能夠將它使用TypeScript Playground運行,查看靜態錯誤提示,進行更深入理解:
interface SuperType {
base: string;
}
interface SubType extends SuperType {
addition: string;
};
// subtype compatibility
let superType: SuperType = { base: 'base' };
let subType: SubType = { base: 'myBase', addition: 'myAddition' };
superType = subType;
// Covariant
type Covariant<T> = T[];
let coSuperType: Covariant<SuperType> = [];
let coSubType: Covariant<SubType> = [];
coSuperType = coSubType;
// Contravariant --strictFunctionTypes true
type Contravariant<T> = (p: T) => void;
let contraSuperType: Contravariant<SuperType> = function(p) {}
let contraSubType: Contravariant<SubType> = function(p) {}
contraSubType = contraSuperType;
// Bivariant --strictFunctionTypes false
type Bivariant<T> = (p: T) => void;
let biSuperType: Bivariant<SuperType> = function(p) {}
let biSubType: Bivariant<SubType> = function(p) {}
// both are ok
biSubType = biSuperType;
biSuperType = biSubType;
// Invariant --strictFunctionTypes true
type Invariant<T> = { a: Covariant<T>, b: Contravariant<T> };
let inSuperType: Invariant<SuperType> = { a: coSuperType, b: contraSuperType }
let inSubType: Invariant<SubType> = { a: coSubType, b: contraSubType }
// both are not ok
inSubType = inSuperType;
inSuperType = inSubType;
複製代碼
咱們將基礎類型叫作T
,複合類型叫作Comp<T>
:
Comp<T>
類型兼容和T
的一致。Comp<T>
類型兼容和T
相反。Comp<T>
類型雙向兼容。Comp<T>
雙向都不兼容。在一些其餘編程語言裏面,使用的是名義類型 Nominal type,好比咱們在Java中定義了一個class Parent
,在語言運行時就是有這個Parent
的類型。所以若是有一個繼承自Parent
的Child
類型,則Child
類型和Parent
就是類型兼容的。可是若是兩個不一樣的class,即便他們內部結構徹底同樣,他倆也是徹底不一樣的兩個類型。
可是咱們知道JavaScript的複雜數據類型Object,是一種結構化的類型。哪怕使用了ES6的class語法糖,建立的類型本質上仍是Object,所以TypeScript使用的也是一種結構化的類型檢查系統 structural typing:
TypeScript uses structural typing. This system is different than the type system employed by some other popular languages you may have used (e.g. Java, C#, etc.)
The idea behind structural typing is that two types are compatible if their members are compatible.
所以在TypeScript中,判斷兩個類型是否兼容,只須要判斷他們的「結構」是否一致,也就是說結構屬性名和類型是否一致。而不須要關心他們的「名字」是否相同。
基於上面這點,咱們能夠來看看TypeScript中那些「奇怪」的疑問:
首先咱們須要知道,函數這一類型是逆變的。
對於協變,咱們很好理解,好比Dog
是Animal
,那Array<Dog>
天然也是Array<Animal>
。可是對於某種複合類型,好比函數。(p: Dog) => void
卻不是(p: Animal) => void
,反過來卻成立。這該怎麼理解?我這裏提供兩種思路:
假設(p: Dog) => void
爲Action<Dog>
,(p: Animal) => void
爲Action<Animal>
。
基於函數的本質
咱們知道,函數就是接收參數,而後作一些處理,最後返回結果。函數就是一系列操做的集合,而對於一個具體的類型Dog
做爲參數,函數不只僅能夠把它當成Animal
,來執行一些操做;還能夠訪問其做爲Dog
獨有的一些屬性和方法,來執行另外一部分操做。所以Action<Dog>
的操做確定比Action<Animal>
要多,所以後者是前者的子集,兼容性是相反的,是逆變。
基於第三方函數對該函數調用
假設有一個函數F
,其參數爲Action<Animal>
,也就是type F = (fp: Action<Animal>) => void
。咱們假設Action<Dog>
與Action<Animal>
兼容,此時咱們若是傳遞Action<Dog>
來調用函數F
,會不會有問題呢?
答案是確定的,由於在函數F
的內部,會對其參數fp
也就是(p: Animal) => void
進行調用,假設F
使用Cat
這一Animal
對其進行調用,若是此時咱們傳遞的參數fp
是(p: Dog) => void
,咱們自己是但願其被調用時傳遞的參數p
是Dog
,然而fp
被調用時卻使用了Cat
,這顯然會使程序崩潰!
所以對於函數這一特殊類型,兼容性須要和其參數的兼容性相反,是逆變。
其次咱們再來看看爲何TS裏的函數還同時支持協變,也就是雙向協變的?
前面提到,TS使用的是結構化類型。所以若是Array<Dog>
和Array<Animal>
兼容,咱們能夠推斷:
Array<Dog>.push
與Array<Animal>.push
兼容
(item: Dog) => number
和(item: Animal) => number
兼容
((item: Dog) => number).arguments
和((item: Animal) => number).arguments
兼容
Dog
和Animal
兼容爲了維持結構化類型的兼容性,TypeScript團隊作了一個權衡 (trade-off)。保持了函數類型的雙向協變性。可是咱們能夠經過設置編譯選項--strictFunctionTypes true
來保持函數的逆變性而關閉協變性。
這個問題其實和函數類型逆變兼容一個道理,也能夠用上述的兩種思路理解,Dog
至關於多個參數,Animal
至關於較少的參數。
從第三方函數調用的角度,若是參數是一個非void的函數。則代表其不關心這個函數參數執行後的返回結果,所以哪怕給一個有返回值的函數參數,第三方的調用函數也不關係,是類型安全的,能夠兼容。
一般狀況下,咱們不須要構造名義類型。可是必定要實現的話,也有一些trick:
名義字符串:
// Strings here are arbitrary, but must be distinct
type SomeUrl = string & {'this is a url': {}};
type FirstName = string & {'person name': {}};
// Add type assertions
let x = <SomeUrl>'';
let y = <FirstName>'bob';
x = y; // Error
// OK
let xs: string = x;
let ys: string = y;
xs = ys;
複製代碼
名義結構體:
interface ScreenCoordinate {
_screenCoordBrand: any;
x: number;
y: number;
}
interface PrintCoordinate {
_printCoordBrand: any;
x: number;
y: number;
}
function sendToPrinter(pt: PrintCoordinate) {
// ...
}
function getCursorPos(): ScreenCoordinate {
// Not a real implementation
return { x: 0, y: 0 };
}
// Error
sendToPrinter(getCursorPos());
複製代碼
TypeScript的類型檢測只是一種編譯時的轉譯,編譯後類型是擦除的,沒法使用JavaScript的instanceof
關鍵字實現類型檢驗:
interface SomeInterface {
name: string;
length: number;
}
interface SomeOtherInterface {
questions: string[];
}
function f(x: SomeInterface|SomeOtherInterface) {
// Can't use instanceof on interface, help?
if (x instanceof SomeInterface) {
// ...
}
}
複製代碼
若是要實現檢測,須要咱們本身實現函數判斷類型內部的結構:
function isSomeInterface(x: any): x is SomeInterface {
return typeof x.name === 'string' && typeof x.length === 'number';
function f(x: SomeInterface|SomeOtherInterface) {
if (isSomeInterface(x)) {
console.log(x.name); // Cool!
}
}
複製代碼
還有更多「奇怪」的疑問,能夠參考TypeScript Wiki FAQs。
最後來聊一下不變性 (Invariant) 的應用。上面咱們提到Array<T>
這一複合類型是協變。可是對於可變數組,協變並不安全。一樣,逆變也不安全(不過通常逆變不存在於數組)。
下面這個例子中運行便會報錯:
class Animal { }
class Cat extends Animal {
meow() {
console.log('cat meow');
}
}
class Dog extends Animal {
wow() {
console.log('dog wow');
}
}
let catList: Cat[] = [new Cat()];
let animalList: Animal[] = [new Animal()];
let dog = new Dog();
// covariance is not type safe
animalList = catList;
animalList.push(dog);
catList.forEach(cat => cat.meow()); // cat.meow is not a function
// contravariance is also not type safe, if it exist here
catList = animalList;
animalList.push(dog);
catList.forEach(cat => cat.meow());
複製代碼
所以,咱們使用可變數組時應該避免出現這樣的錯誤,在作類型兼容的時候儘可能保持數組的不可變性 (immutable)。而對於可變數組,類型本應該作到不變性。可是編程語言中很難實現,在Java中數組類型也都是可變並且協變的。