TypeScript里的類型兼容性是基于結(jié)構(gòu)子類型的。 結(jié)構(gòu)類型是一種只使用其成員來描述類型的方式。 它正好與名義(nominal)類型形成對(duì)比。(譯者注:在基于名義類型的類型系統(tǒng)中,數(shù)據(jù)類型的兼容性或等價(jià)性是通過明確的聲明和/或類型的名稱來決定的。這與結(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中,這段代碼會(huì)報(bào)錯(cuò),因?yàn)镻erson類沒有明確說明其實(shí)現(xiàn)了Named接口。
TypeScript的結(jié)構(gòu)性子類型是根據(jù)JavaScript代碼的典型寫法來設(shè)計(jì)的。 因?yàn)镴avaScript里廣泛地使用匿名對(duì)象,例如函數(shù)表達(dá)式和對(duì)象字面量,所以使用結(jié)構(gòu)類型系統(tǒng)來描述這些類型比使用名義類型系統(tǒng)更好。
TypeScript的類型系統(tǒng)允許某些在編譯階段無法確認(rèn)其安全性的操作。當(dāng)一個(gè)類型系統(tǒng)具此屬性時(shí),被當(dāng)做是“不可靠”的。TypeScript允許這種不可靠行為的發(fā)生是經(jīng)過仔細(xì)考慮的。通過這篇文章,我們會(huì)解釋什么時(shí)候會(huì)發(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
中的每個(gè)屬性,看是否能在y
中也找到對(duì)應(yīng)屬性。 在這個(gè)例子中,y
必須包含名字是name
的string
類型成員。y
滿足條件,因此賦值正確。
檢查函數(shù)參數(shù)時(shí)使用相同的規(guī)則:
function greet(n: Named) {
alert('Hello, ' + n.name);
}
greet(y); // OK
注意,y
有個(gè)額外的location
屬性,但這不會(huì)引發(fā)錯(cuò)誤。 只有目標(biāo)類型(這里是 Named
)的成員會(huì)被一一檢查是否兼容。
這個(gè)比較過程是遞歸進(jìn)行的,檢查每個(gè)成員及子成員。
相對(duì)來講,在比較原始類型和對(duì)象類型的時(shí)候是比較容易理解的,問題是如何判斷兩個(gè)函數(shù)是兼容的。 下面我們從兩個(gè)簡單的函數(shù)入手,它們僅是參數(shù)列表略有不同:
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error
要查看x
是否能賦值給y
,首先看它們的參數(shù)列表。 x
的每個(gè)參數(shù)必須能在y
里找到對(duì)應(yīng)類型的參數(shù)。 注意的是參數(shù)的名字相同與否無所謂,只看它們的類型。 這里, x
的每個(gè)參數(shù)在y
中都能找到對(duì)應(yīng)的參數(shù),所以允許賦值。
第二個(gè)賦值錯(cuò)誤,因?yàn)?code>y有個(gè)必需的第二個(gè)參數(shù),但是x
并沒有,所以不允許賦值。
你可能會(huì)疑惑為什么允許忽略
參數(shù),像例子y = x
中那樣。 原因是忽略額外的參數(shù)在JavaScript里是很常見的。 例如, Array#forEach
給回調(diào)函數(shù)傳3個(gè)參數(shù):數(shù)組元素,索引和整個(gè)數(shù)組。 盡管如此,傳入一個(gè)只使用第一個(gè)參數(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)建兩個(gè)僅是返回值類型不同的函數(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)強(qiáng)制源函數(shù)的返回值類型必須是目標(biāo)函數(shù)返回值類型的子類型。
當(dāng)比較函數(shù)參數(shù)類型時(shí),只有當(dāng)源函數(shù)參數(shù)能夠賦值給目標(biāo)函數(shù)或者反過來時(shí)才能賦值成功。 這是不穩(wěn)定的,因?yàn)檎{(diào)用者可能傳入了一個(gè)具有更精確類型信息的函數(shù),但是調(diào)用這個(gè)傳入的函數(shù)的時(shí)候卻使用了不是那么精確的類型信息。 實(shí)際上,這極少會(huì)發(fā)生錯(cuò)誤,并且能夠?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ù)是可交換的。 原類型上額外的可選參數(shù)并不會(huì)造成錯(cuò)誤,目標(biāo)類型的可選參數(shù)沒有對(duì)應(yīng)的參數(shù)也不是錯(cuò)誤。
當(dāng)一個(gè)函數(shù)有剩余參數(shù)時(shí),它被當(dāng)做無限個(gè)可選參數(shù)。
這對(duì)于類型系統(tǒng)來說是不穩(wěn)定的,但從運(yùn)行時(shí)的角度來看,可選參數(shù)一般來說是不強(qiáng)制的,因?yàn)閷?duì)于大多數(shù)函數(shù)來說相當(dāng)于傳遞了一些undefinded
。
有一個(gè)好的例子,常見的函數(shù)接收一個(gè)回調(diào)函數(shù)并用對(duì)于程序員來說是可預(yù)知的參數(shù)但對(duì)類型系統(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));
對(duì)于有重載的函數(shù),源函數(shù)的每個(gè)重載都要在目標(biāo)函數(shù)上找到對(duì)應(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
類與對(duì)象字面量和接口差不多,但有一點(diǎn)不同:類有靜態(tài)部分和實(shí)例部分的類型。 比較兩個(gè)類類型的對(duì)象時(shí),只有實(shí)例的成員會(huì)被比較。 靜態(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
私有成員會(huì)影響兼容性判斷。 當(dāng)類的實(shí)例用來檢查兼容時(shí),如果它包含一個(gè)私有成員,那么目標(biāo)類型必須包含來自同一個(gè)類的這個(gè)私有成員。 這允許子類賦值給父類,但是不能賦值給其它有同樣類型的類。
因?yàn)門ypeScript是結(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
是兼容的,因?yàn)樗鼈兊慕Y(jié)構(gòu)使用類型參數(shù)時(shí)并沒有什么不同。 把這個(gè)例子改變一下,增加一個(gè)成員,就能看出是如何工作的了:
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
x = y; // error, x and y are not compatible
在這里,泛型類型在使用時(shí)就好比不是一個(gè)泛型類型。
對(duì)于沒指定泛型類型的泛型參數(shù)時(shí),會(huì)把所有泛型參數(shù)當(dāng)成any
比較。 然后用結(jié)果類型進(jìn)行比較,就像上面第一個(gè)例子。
比如,
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里,有兩種類型的兼容性:子類型與賦值。 它們的不同點(diǎn)在于,賦值擴(kuò)展了子類型兼容,允許給 any
賦值或從any
取值和允許數(shù)字賦值給枚舉類型或枚舉類型賦值給數(shù)字。
語言里的不同地方分別使用了它們之中的機(jī)制。 實(shí)際上,類型兼容性是由賦值兼容性來控制的甚至在implements
和extends
語句里。 更多信息,請(qǐng)參閱 TypeScript語言規(guī)范.
更多建議: