TypeScript 類型兼容性

2022-04-21 09:21 更新

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)更好。

關(guān)于可靠性的注意事項

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必須包含名字是namestring類型成員。y滿足條件,因此賦值正確。

檢查函數(shù)參數(shù)時使用相同的規(guī)則:

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

注意,y有個額外的location屬性,但這不會引發(fā)錯誤。 只有目標(biāo)類型(這里是 Named)的成員會被一一檢查是否兼容。

這個比較過程是遞歸進行的,檢查每個成員及子成員。

比較兩個函數(shù)

相對來講,在比較原始類型和對象類型的時候是比較容易理解的,問題是如何判斷兩個函數(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ù)返回值類型的子類型。

函數(shù)參數(shù)雙向協(xié)變

當(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ù)與必須參數(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ù),源函數(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

上面代碼里,xy是兼容的,因為它們的結(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ù)字。

語言里的不同地方分別使用了它們之中的機制。 實際上,類型兼容性是由賦值兼容性來控制的甚至在implementsextends語句里。 更多信息,請參閱 TypeScript語言規(guī)范.


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號