TypeScript里的類型兼容性是基于結(jié)構(gòu)子類型的。 結(jié)構(gòu)類型是一種只使用其成員來描述類型的方式。 它正好與名義(nominal)類型形成對比。(譯者注:在基于名義類型的類型系統(tǒng)中,數(shù)據(jù)類型的兼容性或等價性是通過明確的聲明和/或類型的名稱來決定的。這與結(jié)構(gòu)性類型系統(tǒng)不同,它是基于類型的組成結(jié)構(gòu),且不要求明確地聲明。) 看下面的例子:
interface Named {
name: string;
}
class Person {
name: string;
}
let p: Named;
// OK, because of structural typing
p = new Person();
在使用基于名義類型的語言,比如C#或Java中,這段代碼會報錯,因為Person類沒有明確說明其實現(xiàn)了Named接口。
TypeScript的結(jié)構(gòu)性子類型是根據(jù)JavaScript代碼的典型寫法來設(shè)計的。 因為JavaScript里廣泛地使用匿名對象,例如函數(shù)表達式和對象字面量,所以使用結(jié)構(gòu)類型系統(tǒng)來描述這些類型比使用名義類型系統(tǒng)更好。
TypeScript的類型系統(tǒng)允許某些在編譯階段無法確認(rèn)其安全性的操作。當(dāng)一個類型系統(tǒng)具此屬性時,被當(dāng)做是“不可靠”的。TypeScript允許這種不可靠行為的發(fā)生是經(jīng)過仔細考慮的。通過這篇文章,我們會解釋什么時候會發(fā)生這種情況和其有利的一面。
TypeScript結(jié)構(gòu)化類型系統(tǒng)的基本規(guī)則是,如果x
要兼容y
,那么y
至少具有與x
相同的屬性。比如:
interface Named {
name: string;
}
let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: 'Alice', location: 'Seattle' };
x = y;
這里要檢查y
是否能賦值給x
,編譯器檢查x
中的每個屬性,看是否能在y
中也找到對應(yīng)屬性。 在這個例子中,y
必須包含名字是name
的string
類型成員。y
滿足條件,因此賦值正確。
檢查函數(shù)參數(shù)時使用相同的規(guī)則:
function greet(n: Named) {
alert('Hello, ' + n.name);
}
greet(y); // OK
注意,y
有個額外的location
屬性,但這不會引發(fā)錯誤。 只有目標(biāo)類型(這里是 Named
)的成員會被一一檢查是否兼容。
這個比較過程是遞歸進行的,檢查每個成員及子成員。
相對來講,在比較原始類型和對象類型的時候是比較容易理解的,問題是如何判斷兩個函數(shù)是兼容的。 下面我們從兩個簡單的函數(shù)入手,它們僅是參數(shù)列表略有不同:
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error
要查看x
是否能賦值給y
,首先看它們的參數(shù)列表。 x
的每個參數(shù)必須能在y
里找到對應(yīng)類型的參數(shù)。 注意的是參數(shù)的名字相同與否無所謂,只看它們的類型。 這里, x
的每個參數(shù)在y
中都能找到對應(yīng)的參數(shù),所以允許賦值。
第二個賦值錯誤,因為y
有個必需的第二個參數(shù),但是x
并沒有,所以不允許賦值。
你可能會疑惑為什么允許忽略
參數(shù),像例子y = x
中那樣。 原因是忽略額外的參數(shù)在JavaScript里是很常見的。 例如, Array#forEach
給回調(diào)函數(shù)傳3個參數(shù):數(shù)組元素,索引和整個數(shù)組。 盡管如此,傳入一個只使用第一個參數(shù)的回調(diào)函數(shù)也是很有用的:
let items = [1, 2, 3];
// Don't force these extra arguments
items.forEach((item, index, array) => console.log(item));
// Should be OK!
items.forEach((item) => console.log(item));
下面來看看如何處理返回值類型,創(chuàng)建兩個僅是返回值類型不同的函數(shù):
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});
x = y; // OK
y = x; // Error because x() lacks a location property
類型系統(tǒng)強制源函數(shù)的返回值類型必須是目標(biāo)函數(shù)返回值類型的子類型。
當(dāng)比較函數(shù)參數(shù)類型時,只有當(dāng)源函數(shù)參數(shù)能夠賦值給目標(biāo)函數(shù)或者反過來時才能賦值成功。 這是不穩(wěn)定的,因為調(diào)用者可能傳入了一個具有更精確類型信息的函數(shù),但是調(diào)用這個傳入的函數(shù)的時候卻使用了不是那么精確的類型信息。 實際上,這極少會發(fā)生錯誤,并且能夠?qū)崿F(xiàn)很多JavaScript里的常見模式。例如:
enum EventType { Mouse, Keyboard }
interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number }
interface KeyEvent extends Event { keyCode: number }
function listenEvent(eventType: EventType, handler: (n: Event) => void) {
/* ... */
}
// Unsound, but useful and common
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));
// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));
// Still disallowed (clear error). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e));
比較函數(shù)兼容性的時候,可選參數(shù)與必須參數(shù)是可交換的。 原類型上額外的可選參數(shù)并不會造成錯誤,目標(biāo)類型的可選參數(shù)沒有對應(yīng)的參數(shù)也不是錯誤。
當(dāng)一個函數(shù)有剩余參數(shù)時,它被當(dāng)做無限個可選參數(shù)。
這對于類型系統(tǒng)來說是不穩(wěn)定的,但從運行時的角度來看,可選參數(shù)一般來說是不強制的,因為對于大多數(shù)函數(shù)來說相當(dāng)于傳遞了一些undefinded
。
有一個好的例子,常見的函數(shù)接收一個回調(diào)函數(shù)并用對于程序員來說是可預(yù)知的參數(shù)但對類型系統(tǒng)來說是不確定的參數(shù)來調(diào)用:
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));
// Confusing (x and y are actually required) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));
對于有重載的函數(shù),源函數(shù)的每個重載都要在目標(biāo)函數(shù)上找到對應(yīng)的函數(shù)簽名。 這確保了目標(biāo)函數(shù)可以在所有源函數(shù)可調(diào)用的地方調(diào)用。
枚舉類型與數(shù)字類型兼容,并且數(shù)字類型與枚舉類型兼容。不同枚舉類型之間是不兼容的。比如,
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
let status = Status.Ready;
status = Color.Green; //error
類與對象字面量和接口差不多,但有一點不同:類有靜態(tài)部分和實例部分的類型。 比較兩個類類型的對象時,只有實例的成員會被比較。 靜態(tài)成員和構(gòu)造函數(shù)不在比較的范圍內(nèi)。
class Animal {
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
私有成員會影響兼容性判斷。 當(dāng)類的實例用來檢查兼容時,如果它包含一個私有成員,那么目標(biāo)類型必須包含來自同一個類的這個私有成員。 這允許子類賦值給父類,但是不能賦值給其它有同樣類型的類。
因為TypeScript是結(jié)構(gòu)性的類型系統(tǒng),類型參數(shù)只影響使用其做為類型一部分的結(jié)果類型。比如,
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // okay, y matches structure of x
上面代碼里,x
和y
是兼容的,因為它們的結(jié)構(gòu)使用類型參數(shù)時并沒有什么不同。 把這個例子改變一下,增加一個成員,就能看出是如何工作的了:
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
x = y; // error, x and y are not compatible
在這里,泛型類型在使用時就好比不是一個泛型類型。
對于沒指定泛型類型的泛型參數(shù)時,會把所有泛型參數(shù)當(dāng)成any
比較。 然后用結(jié)果類型進行比較,就像上面第一個例子。
比如,
let identity = function<T>(x: T): T {
// ...
}
let reverse = function<U>(y: U): U {
// ...
}
identity = reverse; // Okay because (x: any)=>any matches (y: any)=>any
目前為止,我們使用了兼容性
,它在語言規(guī)范里沒有定義。 在TypeScript里,有兩種類型的兼容性:子類型與賦值。 它們的不同點在于,賦值擴展了子類型兼容,允許給 any
賦值或從any
取值和允許數(shù)字賦值給枚舉類型或枚舉類型賦值給數(shù)字。
語言里的不同地方分別使用了它們之中的機制。 實際上,類型兼容性是由賦值兼容性來控制的甚至在implements
和extends
語句里。 更多信息,請參閱 TypeScript語言規(guī)范.
更多建議: