TypeScript手冊翻譯系列10-類型兼容性

類型兼容性(Type Compatibility)

TypeScript中類型兼容性是基於structural subtyping。Structural typing是一種僅根據成員來關聯類型的方式,這與nominal typing正好相反。考慮下面代碼:
typescript

interface Named {
   name: string;
}

class Person {
   name: string;
}

var p: Named;
// OK, because of structural typing
p = new Person();


在C#或Java等nominally-typed語言中,上面的代碼會報錯,由於Person類沒有明確描述本身是Named接口的實現。

TypeScript的structural類型系統的設計是基於JavaScript代碼一般是如何編寫的。 因爲JavaScript普遍使用函數表達式和對象字面量(object literal)等匿名對象,所以利用structural類型系統而不是nominal類型系統能夠更天然地表示JavaScript庫中存在的各類類型間的關係。

編程

關於健壯性(A Note on Soundness)

TypeScript的類型系統容許在編譯時還不知道是否安全的一些操做。當一個類型系統有這種特性時,就稱爲不是健壯的(「sound「)。TypeScript中容許不健壯行爲的地方都通過仔細考慮,本章節中將解釋這些地方,以及背後的動機場景。
數組

Starting out

TypeScrip的structural類型系統的基本規則是:若是y中包含x中相同的成員(換另外一種說法:y中除了包含x中相同的成員之外,可能還存在其餘成員),那麼就稱x與y兼容(x is compatible with y)。例如:
安全

interface Named {
   name: string;
}

var x: Named;
// y’s inferred type is { name: string; location: string; }
var y = { name: 'Alice', location: 'Seattle' };
x = y;


爲了檢測y是否能夠被賦值給x,編譯器檢查x的每一個屬性,發如今y中都有對應的兼容屬性。這種狀況下,y必須有一個成員‘name’,其類型必須是string。的確有,所以容許賦值。

當檢測函數調用參數時,一樣的賦值規則適用:

less

function greet(n: Named) {
   alert('Hello, ' + n.name);
}

greet(y); // OK


注意‘y’還有另一個‘location’屬性,但這不會報錯。當檢測兼容性時只考慮target類型(代碼中是‘Named’ )的成員。

這個比較過程遞歸處理,檢查每個成員以及子成員的類型。

ide

比較兩個函數(Comparing two functions)

雖然比較基本類型(primitive types)與對象類型相對顯而易見,但什麼樣的函數被視爲兼容這一問題就有些棘手。先從一個簡單例子開始,兩個函數只有參數列表不一樣:
函數

var x = (a: number) => 0;
var y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error


爲了檢查x是否能夠被賦值給y,咱們首先來看參數列表。y中的每一個參數都必須有x中對應的參數且類型兼容。注意不考慮參數名稱,只考慮參數類型。因爲x中每一個參數在y中都有對應的類型兼容參數,所以容許賦值。

第二個賦值是錯誤的,由於y還須要第二個參數但‘x’沒有這個參數,因此不容許賦值。

你可能奇怪在這個例子中y = x爲何要容許丟棄(‘discarding’)參數。容許賦值的緣由就在於JavaScript中忽略額外的函數參數是很是廣泛的。例如Array#forEach 提供了三個參數給回調函數:數組元素、索引、以及包含元素的數組。但提供一個僅使用第一個參數的回調函數是頗有用的:

ui

var 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));


如今來看如何處理返回類型,使用兩個只是返回類型不一樣的函數:
spa

var x = () => ({name: 'Alice'});
var y = () => ({name: 'Alice', location: 'Seattle'});

x = y; // OK
y = x; // Error because x() lacks a location property


類型系統強行要求源函數(source function)的返回類型必須是目標函數(target function)返回類型的子類型。
.net

Function Argument Bivariance

當比較函數參數類型時,若是源參數(source parameter )可賦值給目標參數( target parameter),或者反過來目標參數可賦值給源參數,那麼賦值成立。這是不健壯的,由於一個調用方可能給了一個接受更專用類型的函數a function that takes a more specialized type, 但調用時用一個不專用類型的函數(invokes the function with a less specialized type)。實際中,這種錯誤很是罕見,容許這一點可使許多常見的JavaScript patterns成立。看一個簡短的例子:

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));

可選參數與Rest參數(Optional Arguments and Rest Arguments)

當比較函數兼容性時,可選參數與必選參數是可互換的。源類型額外的可選參數不是一個錯誤,目標類型的可選參數在目標類型中沒有對應的參數不是一個錯誤。

當函數有一個rest參數時,這個參數被視爲它是一個無窮系列的可選參數。

從類型系統視角來看這是不健壯的,但從運行時觀點來看,可選參數通常沒有well-enforced,由於對大多數函數來講等同於在這個位置上傳遞‘undefined’。

下面這個例子是函數的常見模式,接受一個回調,可是用某種(對編程人員來講)可預測的但(對類型系統來講)未知數量的參數來激活:

function invokeLater(args: any[], callback: (...args: any[]) => void) {    
   /* ... Invoke callback with 'args' ... */
}

// Unsound - invokeLater "might" provide any number of arguments
// 不健壯- invokeLater可能提供任意數量的參數
invokeLater([1, 2], (x, y) => console.log(x + ', ' + y));

// Confusing (x and y are actually required) and undiscoverable
// 使人疑惑(x與y其實是必填參數),說不清道不明
invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));

函數重載(Functions with overloads)

當函數有重載時,在源類型中的每一個重載函數都必須與目標類型中的兼容簽名匹配。這確保目標函數能夠同源函數同樣在各類狀況下被調用。有特殊重載簽名的函數(Functions with specialized overload signatures),即在重載中使用串字面量(string literals)的函數,當檢查兼容性時不使用特殊簽名(do not use their specialized signatures when checking for compatibility)。

枚舉(Enums)

枚舉與number兼容,number與枚舉兼容。不一樣枚舉類型的枚舉值被視爲不兼容。例如:

enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };

var status = Status.Ready;
status = Color.Green;  //error

類(Classes)

類的工做與對象字面量類型和接口類似, 但有一個不一樣:它們都有一個靜態與一個實例類型。當比較一個class類型的兩個對象時,只比較實例的成員,靜態成員與構造函數不影響兼容性。

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

類中的私有成員(Private members in classes)

類中的私有成員影響兼容性。當檢查一個類實例的兼容性時,若是它包含私有成員,目標類型也必須包含源於同一個類的私有成員。這樣就容許一個類賦值給與其超類兼容的變量,當與具備相同shape但有不一樣繼承關係的類是不兼容的。

泛型(Generics)

因爲TypeScript是一個structural類型系統,當類型參數用做一個成員的部分類型時,它隻影響最終的類型(type parameters only affect the resulting type when consumed as part of the type of a member)。例如:

interface Empty<T> {
}

var x: Empty<number>;
var y: Empty<string>;

x = y;  // okay, y matches structure of x


在上面例子中,x與y兼容,緣由是它們的結構並非以不一樣方式來使用類型參數。修改前面例子,添加一個成員到Empty<T>中,看看結果如何:

interface NotEmpty<T> {
   data: T;
}

var x: NotEmpty<number>;
var y: NotEmpty<string>;

x = y;  // error, x and y are not compatible

此時,一個帶類型參數的泛型類型的行爲就如同一個非泛型類型。

對於沒有指定類型參數的泛型類型來講,用'any'替換全部未指定的類型參數來檢查兼容性。 而後對最終類型(The resulting types)檢查兼容性,就像非泛型類型同樣。
例如:

var identity = function<T>(x: T): T { 
   // ...
}

var reverse = function<U>(y: U): U {    
   // ...
}

identity = reverse;  // Okay because (x: any)=>any matches (y: any)=>any

高級話題(Advanced Topics)

子類型與賦值(Subtype vs Assignment)

上面,咱們一直在使用兼容性('compatible'),這在語言規範中並無術語來定義。在TypeScript中有兩種兼容性:子類型與賦值(subtype and assignment)。它們的區別在於:賦值擴展了子類型兼容性,容許to and from 'any'賦值,容許to and from enum用對應的數值賦值。 

語言中不一樣的地方使用這兩種兼容性中的一種,要視狀況而定。在實際狀況中,類型兼容性是由賦值兼容性所支配,即便在實現與擴展語句等狀況下。更多信息可參見 
TypeScript spec

參考資料

[1] http://www.typescriptlang.org/Handbook#type-compatibility

[2] TypeScript系列1-簡介及版本新特性, http://my.oschina.net/1pei/blog/493012

[3] TypeScript手冊翻譯系列1-基礎類型, http://my.oschina.net/1pei/blog/493181

[4] TypeScript手冊翻譯系列2-接口, http://my.oschina.net/1pei/blog/493388

[5] TypeScript手冊翻譯系列3-類, http://my.oschina.net/1pei/blog/493539

[6] TypeScript手冊翻譯系列4-模塊, http://my.oschina.net/1pei/blog/495948

[7] TypeScript手冊翻譯系列5-函數, http://my.oschina.net/1pei/blog/501273

[8] TypeScript手冊翻譯系列6-泛型, http://my.oschina.net/1pei/blog/513483

[9] TypeScript手冊翻譯系列7-常見錯誤與mixins, http://my.oschina.net/1pei/blog/513499

[10] TypeScript手冊翻譯系列8-聲明合併, http://my.oschina.net/1pei/blog/513649

[11] TypeScript手冊翻譯系列9-類型推斷, http://my.oschina.net/1pei/blog/513652

相關文章
相關標籤/搜索