TypeScript中的類型兼容是基於結構歸類的。在普通分類的相比之下,結構歸類是一種純粹用於將其成員的類型進行關聯的方法。思考下面的代碼:html
interface Named { name: string; } class Person { name: string; } var p: Named; // 正確, 由於這裏編譯器自動進行結構歸類 p = new Person();
如C#、Java這些表面上的類型語言(這裏指的「表面上的類型語言」,指C#和Java須要使用「implements」關鍵字明確指出類實現某個接口才能對應得上其類型),以上的代碼便會被看成錯誤的,由於沒有明確指出Person類實現(implements)Named接口。數組
TypeScript的結構類型系統就是基於JavaScript代碼典型的寫法設計的。由於JavaScript普遍使用匿名對象如函數表達式和字面量對象,使用結構類型系統代替表面上處理將使JavaScript中這些關係體現的更天然。安全
TypeScript類型系統容許執行某些在編譯階段沒法肯定安全性的操做。當一個類型系統有這個屬性的時候,咱們視之爲「不健全的」。TypeScript中容許執行這些操做這一機制是通過仔細考慮的,經過文檔咱們將解釋什麼狀況下會發生這種事和容許這些操做後所帶來的好的一面。app
跟着代碼出發(老司機,帶帶我...)ide
TypeScript結構類型系統的基本規則如:若是x是兼容y的,那麼y至少具備和x相同的屬性成員。例如:函數
interface Named { name: string; } var x: Named; // 推斷出y的相似是{ name: string; location: string; } var y = { name: 'Alice', location: 'Seattle' }; x = y;
爲了檢查y是否可以賦值給x,編譯器須要檢查x的每一個屬性,而且在y中找到對應的兼容屬性。在這種狀況下,y必須有個名爲name而且值是字符串的屬性。而y知足了這條件,因此可以賦值給x。spa
一樣的規則也適用在檢查函數調用參數時:設計
// 接着上面的代碼 function greet(n: Named) { alert('Hello, ' + n.name); } greet(y); // ok
注意,y有一個額外的"location'屬性,但這並未產生錯誤。只有目標類型的成員(這裏的目標類型指「Named」)會被檢查是否兼容。code
比較的過程是遞歸進行的,檢查每一個成員及其子成員的類型。htm
兩個函數之間的比較
原始類型和對象類型的比較是相對簡單的,但問題是被認爲是兼容的函數是怎麼樣的呢。讓咱們從一個最基本的例子開始吧,如下兩個函數的不一樣之處僅僅在於他們的參數列表:
var x = (a: number) => 0; var y = (b: number, s: string) => 0; y = x; // ok x = y; // 錯誤
若要檢查x是否能夠賦值給y,首先看參數列表。y中的每一個參數必須在x中都有相應而且類型兼容的參數。主意,參數名可不考慮,只要類型可以對應上。在這個案例中,x的每一個參數在y中都有相應的參數,因此是容許賦值的。第二個賦值是錯誤的,由於y的第二個屬性是必須的,可是x沒這個屬性,因此不被容許賦值。
你可能對爲何在y=x中容許第二個參數而感到疑惑。賦值的過程容許忽略函數額外的參數,這在JavaScript中實際上很常見。例如Array的forEach函數爲他的回調函數提供了三個參數:數組元素、索引、包含它的數列。然而,大多用到的也就第一個參數。
var items = [1, 2, 3]; // 這些參數不是強制要求的 items.forEach((item, index, array) => console.log(item)); // 這樣也能夠 items.forEach((item) => console.log(item));
如今讓咱們來看看返回值類型是如何處理的,使用兩個僅返回值類型不一樣的函數:
var x = () => ({name: 'Alice'}); var y = () => ({name: 'Alice', location: 'Seattle'}); x = y; // ok y = x; // 錯誤,由於x()缺乏一個屬性
類型機制強制要求源函數的返回值類型是目標函數返回值類型的子類型。
可選參數及剩餘參數
當對函數的兼容進行比較時,可選和必須的參數是能夠互換的。源類型有額外的可選參數不會形成錯誤,目標類型的可選參數中不存在對應參數也不會產生錯誤。
當函數有其他的參數,將會被看成無限的可選參數同樣來處理。
從類型機制來看這是不健全的,但從代碼運行的角度看,可選參數不是強制要求的,由於它至關於在函數參數的對應位置傳入一個"undefined"。
下面的例子是個廣泛模式的函數,該函數須要傳入個回調函數,而且在調用的時候傳入可預知(對於開發者而言)可是未知數量(對於類型機制)的參數。
function invokeLater(args: any[], callback: (...args: any[]) => void):void { callback.apply(null,args); } // invokeLater"可能"任何數量的參數 invokeLater([1, 2], (x, y) => console.log(x , y)); invokeLater([3], (x?, y?) => console.log(x , y)); invokeLater([4,5,6,7], (x?, y?) => console.log(x , y));
重載的函數
當一個函數具備重載狀況時,源類型的每次重載必須在目標類型上可找到匹配的簽名。這確保了目標函數能夠在全部源函數可調用的地方調用。當作兼容性檢查時,帶有特殊簽名的函數重載(那些重載時使用字符串)將不會使用他們特殊簽名。(詳情可見:TypeScript Declaration Merging(聲明合併)中的接口合併第二個案例)
枚舉
枚舉和number相互兼容。不一樣枚舉類型的枚舉值之間是不兼容的。例如:
enum Status { Ready, Waiting }; enum Color { Red, Blue, Green }; var status = Status.Ready; status = Color.Green; // 錯誤
類
類的兼容和對象字面量類型還有接口的兼容類似,只是有一個不一樣:它具備靜態類型和實例類型。比較兩個類類型的對象時,只比較實例部分的成員。靜態部分的成員和構造函數不影響兼容性。
class Animal { feet: number; constructor(name: string, numFeet: number) { } } class Size { feet: number; constructor(numFeet: number) { } } var a: Animal; var s: Size; a = s; // ok s = a; // ok
類的私有成員
類中的私有成員會影響其兼容性。當一個類的實例進行兼容性檢查時,若是它包含一個私有成員,那麼目標類型必須也包含一個來源與同一個類的私有成員(詳情可參閱:TypeScript Class(類)中的理解Private(私有))。這也形成了一個類能夠被賦值爲其父類的實例,可是卻不能被賦值成另外一個繼承其父類的類(雖然他們是同一個類型)。
泛型
由於TypeScript是一個結構類型系統,參數類型隻影響將其做爲部分紅員類型的目標類型(好比有個函數fn(a:string),而後函數中有個變量的某屬性值是a,那麼a對這個目標類型將產生影響)。
案例:
interface Empty<T> { } var x: Empty<number>; var y: Empty<string>; x = y; // ok, y能夠和x的結構相匹配
在上述例子中,x和y是兼容的,由於他們的結構在使用類型參數並沒什麼不一樣之處。改變這個例子,給Empty<T>加個成員,看看會是什麼狀況:
interface NotEmpty<T> { data: T; } var x: NotEmpty<number>; var y: NotEmpty<string>; x = y; // 錯誤,x和y不兼容
對於那些沒有指定其參數類型的泛型類型,兼容性的檢查是經過爲全部沒指定參數類型的參數使用"any"代替的。而後目標類型檢查兼容性,就和非泛型的案例通常。
案例:
var identity = function<T>(x: T): T { // ... } var reverse = function<U>(y: U): U { // ... } identity = reverse; // ok,由於(x: any)=>any和(y: any)=>any能夠匹配
深層探討
子類VS賦值
至此爲止,咱們瞭解了兼容性,它並未在語言規範裏定義。在TypeScript中有兩種兼容:子類和賦值。它們的不一樣點在於,賦值擴展了子類型的兼容性而且可對"any"進行取值和賦值、對枚舉進行取對應數值。
根據狀況的不一樣,TypeScript語言會在不一樣的地方使用兩種兼容機制中的一種。對於實際運用而言,類型的兼容性是由賦值時的兼容檢查或者implements和extends關鍵字來控制的。更多詳情,請參照TypeScript spec (友情提醒,是doc文件下載)