一。類型推斷
賦值推斷
類型推斷機制減輕了強類型帶來的語法負擔,例如:
let x = 3;
// 等價於
let x: number = 3;
編譯器能夠根據變量初始值 3 推斷出變量類型是 number,因此多數場景下不必顯式聲明類型,它猜得到
P.S. 即使在一切都要提前確定類型的 [Haskell](/articles/類型-haskell 筆記 3/) 中,也並非處處充斥著類型聲明,而是相當簡潔,正是因為編譯器提供了強大的類型推斷支持
在類似賦值的場景能夠根據目標值來確定類型,具體如下:
-
變量或(類)成員初始值
-
參數默認值
-
函數返回值
這 3 類值都能提供直接的類型信息,進而確定目標類型。除此之外,還有一些不那麼直接的場景,比如數組類型
數組類型
let x = [0, 1, null];
數組中的元素除了 number 就是 null,而 number「兼容」null,因此推斷 x 的類型是 number[]
Null、Undefined 和 Never 是其它類型的子類型,因此可以賦值給任何其它類型變量
(摘自 [基本類型_TypeScript 筆記 2](/articles/基本類型-typescript 筆記 2/#articleHeader3))
也就是說,要確定數組類型的話,先要確定每個元素的類型,再考慮其兼容關係,最終確定一個最「寬」的類型(包容數組中所有其它類型,稱為 best common type)作為數組類型
如果數組元素中沒有一個能夠兼容其它所有類型的類型(即找不出 best common type),就用聯合類型,例如:
// 推斷 mixin: (string | number | boolean)[]
let mixin = [1, '2', true];
class Animal {}
class Elephant extends Animal {}
class Snake extends Animal {}
// 推斷 zoo: (Elephant | Snake)[]
let zoo: Animal[] = [new Elephant(), new Snake()];
上下文推斷
與賦值推斷相比,上下文推斷是另一種不同的思路:
推斷
值 ------> 變量類型
查找 匹配(推斷)
上下文 -----> 上下文類型 -----------> 變量類型
前者從值到類型,後者從類型到類型。根據上下文得出類型信息,再按位置映射到變量上,例如:
// 推斷 mouseEvent: MouseEvent
window.onmousedown = function(mouseEvent) {
// ...
};
右側匿名函數作為 mousedown 事件處理器,遵從 DOM API 的類型約束,從而得出參數類型:
interface MouseEvent extends UIEvent {
readonly clientX: number;
readonly clientY: number;
//...等等很多屬性
}
interface GlobalEventHandlers {
onmousedown: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
}
interface Window extends GlobalEventHandlers {/*...*/}
declare var window: Window;
(摘自 TypeScript/lib/lib.dom.d.ts)
如果脫離了 mousedown 事件處理器這個上下文,就推斷不出參數類型了:
// 推斷 mouseEvent: any
function handler(mouseEvent) {
console.log(mouseEvent.clickTime);
}
很多場景都會根據上下文推斷類型,例如:
-
函數調用中的參數
-
賦值語句的右側
-
類型斷言
-
對象成員和數組字面量
-
return語句
二。子類型兼容性
[TypeScript 的 13 種基本類型](/articles/基本類型-typescript 筆記 2/) 中,類型層級關係如下:
[caption id="attachment_1899" align="alignnone" width="625"]
TypeScript 類型關係[/caption]
簡單總結:
-
Any 最「寬」。兼容其它所有類型(換言之,其它類型都是 Any 的子類型)
-
Never 最「窄」。不兼容任何其它類型
-
Void 兼容 Undefined 和 Null
-
其它類型都兼容 Never 和 Void
P.S. 兼容可以簡單理解可否賦值(文末有嚴謹描述),例如:
let x: any;
let y: number;
let z: null;
// Any 兼容 Number
x = y;
// Number 兼容 Null
y = z;
// Null 不兼容 Number
// 錯誤 Type 'number' is not assignable to type 'null'.
z = y;
不只基本類型有層級,函數、類、泛型等複雜類型間也有這樣的兼容關係
三。函數
兼容性判定
對類型系統而言,需要準確判斷一個函數類型是否兼容另一個函數類型,例如在賦值的場景:
let log: (msg: string) => void
let writeToFile: (msg: any, encode: string) => void
// 類型兼容嗎?該該不該報錯
log = writeToFile;
writeToFile = log;
從類型安全角度來看,把 log 換成 writeToFile 不安全(缺 encode 參數,writeToFile 不一定能正常工作),反過來的話是安全的,因為返回值類型相同,參數綽綽有餘,msg 的類型也兼容(string 是 any 的子類型)
具體的,TypeScript 類型系統對函數類型的兼容性判定規則如下:
-
參數:要求對應參數的類型兼容,數量允許多餘
let x = (a: number) => 0; let y = (b: number, s: string) => 0; y = x; // OK x = y; // Error -
返回值:要求返回值類型兼容
let x = () => ({name: "Alice"}); let y = () => ({name: "Alice", location: "Seattle"}); x = y; // OK y = x; // Error, because x() lacks a location property -
函數類型:要求滿足雙變性約束
函數類型的雙變性(bivariance)
雙變是指同時滿足協變和逆變,簡單地講:
-
協變(covariant):允許出現父類型的地方,也允許出現子類型,即 里氏替換原則
-
逆變(contravariant):協變反過來,即允許出現子類型的地方,也允許出現父類型
-
雙變(bivariant):同時滿足協變和逆變
-
不變(invariant 或 nonvariant):既不滿足協變也不滿足逆變
協變很容易理解,子類型兼容父類型,此外還具有一些(父類型不具備的)擴展屬性或方法,因此用子類型換掉父類型後,仍能正常工作(類型安全)
而逆變並不很直觀,什麼場景下,用父類型換掉子類型後,仍能保證類型安全呢?
繼承關係中的成員函數重寫,算是逆變的典型例子:
class Example {
foo(maybe: number | undefined) { }
str(str: string) { }
compare(ex: Example) { }
}
class Override extends Example {
foo(maybe: number) { } // Bad: should have error.
str(str: 'override') { } // Bad: should have error.
compare(ex: Override) { } // Bad: should have error.
}
(摘自 Overridden method parameters are not checked for parameter contravariance)
對比重寫前後的函數類型:
// foo
(maybe: number | undefined) => any
(maybe: number) => any
// str
(str: string) => any
(str: 'override') => any
// compare
(ex: Example) => any
(ex: Override) => any
P.S.str(str: 'override') 比 str(str: string)「窄」個 undefined,默認值使得參數值集少了 undefined
參數都從「寬」的類型變成了更「窄」的類型,即從父類型變為子類型,顯然,這樣做是不安全的,例如:
function callFoo(example: Example) {
return example.foo(undefined);
}
callFoo(new Example()); // 沒問題
callFoo(new Override()); // 可能會出錯,因為子類的 foo 不接受 undefined
相反地,如果子類重寫後的參數類型更「寬」,那麼就是安全的,例如:
class Example {
foo(maybe: number | undefined) { }
}
class Override extends Example {
foo(maybe: number) { } // Sound
}
這就是所謂的逆變,對成員函數的參數而言,用父類型換掉子類型是安全的,即:
允許出現子類型的地方,也允許出現父類型
從類型角度來看,子類型允許類型之間有層級(繼承)關係,從寬泛類型到特殊類型,而協變、逆變等關係就建立在這種類型層級之上:
-
協變:簡單類型的層級關係保留到了複雜類型,這個複雜類型就是協變的,例如
Animal是Cat的父類型,而數組Animal[]也是Cat[]的父類型,所以數組類型是協變的 -
逆變:簡單類型的層級關係到複雜類型中反過來了,這個複雜類型就是逆變的。例如函數類型
Animal => string是Cat => string的子類型(因為後者接受的參數更「窄」),而簡單類型Animal是Cat的父類型,那麼函數類型就是逆變的
P.S. 如我們所見,逆變並不直觀,因此為了保持類型系統簡單,有的語言也認為類型構造器不變,雖然這樣可能會違反類型安全
特殊地,TypeScript 裡的函數類型是雙變的,例如:
interface Comparer<T> {
compare(a: T, b: T): number;
}
declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;
animalComparer = dogComparer; // Ok because of bivariance
dogComparer = animalComparer; // Ok
理論上應該要求函數參數逆變,以確保類型安全,因此:
// 把父類型賦值給子類型,在逆變的場景中是安全的
dogComparer = animalComparer; // Ok
// 把子類型賦值給父類型,在逆變的場景(函數類型)中是不安全的
animalComparer = dogComparer; // Ok because of bivariance
後者不安全,但在 JavaScript 世界裡很常見:
This is unsound because a caller might end up being given a function that takes a more specialized type, but invokes the function with a less specialized type. In practice, this sort of error is rare, and allowing this enables many common JavaScript patterns.
所以 TypeScript 並沒有強制約束函數類型逆變,而是允許雙變。更進一步地,在比較兩個函數類型時,只要一方參數兼容另一方的參數即可,如上例中 dogComparer 與 animalComparer 能夠相互賦值
可選參數和剩餘參數
比較參數兼容性時,不要求匹配可選參數,比如原類型具有額外的可選參數是合法的,目標類型缺少相應的可選參數也是合法的
對於剩餘參數,就當成是無限多個可選參數,也不要求嚴格匹配。雖然從類型系統的角度來看不安全,但在實際應用中是一種相當常見的「模式」,例如用不確定的參數調用回調函數:
function invokeLater(args: any[], callback: (...args: any[]) => void) {
/* ... Invoke callback with 'args' ... */
}
// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ", " + y));
函數重載
對於存在多個重載的函數,要求源函數的每個重載版本在目標函數上都有對應的版本,以保證目標函數可以在所有源函數可調用的地方調用,例如:
interface sum {
(a: number, b: number): number;
(a: number[]): number;
}
// Sum 要求的兩個版本
function add(a: any, b: any);
function add(a: any[], b?: any): any;
// 額外的版本
function add(a: any[], b: any, c: number): any;
function add(a, b) { return a; }
let sum: sum = add;
sum 函數有兩個重載版本,所以目標函數至少要兼容這兩個版本
四。枚舉
首先,來自不同枚舉類型的枚舉值不兼容,例如:
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
let s = Status.Ready;
// Type 'Color.Green' is not assignable to type 'Status'.
s = Color.Green; // Error
特殊地,數值枚舉 與數值相互類型兼容,例如:
enum Status { Ready, Waiting };
// 數值兼容枚舉值
let ready: number = Status.Ready;
// 枚舉值兼容數值
let waiting: Status = 1;
但 字符串枚舉 並不與字符串類型相互兼容
enum Status { Ready = '1', Waiting = '0' };
let ready: string = Status.Ready;
// 報錯 Type '"0"' is not assignable to type 'Status'.
let waiting: Status = '0';
P.S. 雖然從實際類型上看,上例賦值是合法的,但在類型系統中認為二者不兼容,因此報錯
五。類
類與對象字面量類型和接口類似,區別在於,類同時具有實例類型和靜態類型,而比較兩個類實例時,僅比較實例成員
因此,靜態成員和構造函數並不影響兼容性:
class Animal {
static id: string = 'Kitty';
feet: number;
constructor(name: string, numFeet: number) { }
}
class Size {
feet: number;
constructor(numFeet: number) { }
}
let a: Animal;
let s: Size;
a = s; // OK
s = a; // OK
私有成員和受保護成員
成員修飾符 private 和 protected 也會影響類型兼容性,具體地,要求這些成員源自同一個類,以此保證父類兼容子類:
class Animal {
private feet: number;
constructor() { }
}
class Cat extends Animal { }
// 和 Animal 長得完全一樣的 Tree
class Tree {
private feet: number;
constructor() { }
}
// 正確 父類兼容子類
let animal: Animal = new Cat();
// 錯誤 Type 'Tree' is not assignable to type 'Animal'.
animal = new Tree();
// 正確 二者「形狀」完全一樣
let kitty: Cat = new Animal();
把 Tree 實例賦值給 Animal 類型會報錯,雖然二者看起來完全一樣,但私有屬性 feet 源自不同的類:
Types have separate declarations of a private property 'feet'.
同樣的,上例中把 Animal 實例賦值給 Cat 類型之所以不報錯,是因為二者成員列表相同,並且私有屬性 feet 也源自同一個 Animal 類
六。泛型
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // OK, because y matches structure of x
y = x; // OK, because x matches structure of y
儘管 Empty<number> 與 Empty<string> 差別很大,但泛型定義中並沒有用到類型參數 T(類似於 unused variable,沒什麼意義),因此互相兼容
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
// 錯誤 Type 'Empty<string>' is not assignable to type 'Empty<number>'.
x = y;
此時,指定了類型參數的泛型與一般具體類型一樣嚴格比較,對於未指定類型參數的泛型,就當類型參數是 any,再進行比較,例如:
let identity = function<T>(x: T): T {
//...
return x;
}
let reverse = function<U>(y: U): U {
//...
return y;
}
// 正確 等價於把 (y: any) => any 賦值給 (x: any) => any
identity = reverse;
七。類型兼容性
實際上,TypeScript 規範中只定義了 2 種兼容性,子類型兼容性與賦值兼容性,二者存在細微的區別:
Assignment extends subtype compatibility with rules to allow assignment to and from any, and to and from enum with corresponding numeric values.
賦值兼容性擴展了子類型兼容性,允許 any 相互賦值,以及 enum 和對應數值相互賦值
至於類型兼容性,規範中並未定義這個概念,在多數語境下,所謂的類型兼容性遵從賦值兼容性,implements 和 extends 子句也不例外
暫無評論,快來發表你的看法吧