類型相容性

TypeScript 中的類型相容性基於結構化子類型。結構化類型是一種僅根據成員關聯類型的途徑。這與名義類型形成對比。考慮以下程式碼

ts
interface Pet {
name: string;
}
class Dog {
name: string;
}
let pet: Pet;
// OK, because of structural typing
pet = new Dog();

在像 C# 或 Java 等名義類型語言中,等效程式碼會是一個錯誤,因為 Dog 類別並未明確描述自己是 Pet 介面的實作方。

TypeScript 的結構化類型系統是根據 JavaScript 程式碼的典型寫法而設計的。由於 JavaScript 廣泛使用匿名物件,例如函式表達式和物件文字,因此使用結構化類型系統而不是名義類型系統來表示 JavaScript 函式庫中找到的關係類型更加自然。

健全性注意事項

TypeScript 的類型系統允許某些作業在編譯時無法得知為安全。當類型系統具有此屬性時,表示它「不健全」。TypeScript 允許不健全行為的地方經過仔細考量,我們將在本文檔中說明這些情況發生的位置及其背後的動機情境。

開始

TypeScript 結構類型系統的基本規則是,如果 y 至少具有與 x 相同的成員,則 xy 相容。例如,考慮以下涉及名為 Pet 的介面的程式碼,該介面具有 name 屬性

ts
interface Pet {
name: string;
}
let pet: Pet;
// dog's inferred type is { name: string; owner: string; }
let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
pet = dog;

若要檢查是否可以將 dog 指定給 pet,編譯器會檢查 pet 的每個屬性,以在 dog 中找到對應的相容屬性。在這種情況下,dog 必須有一個名為 name 的成員,該成員是一個字串。它有,因此允許指定。

檢查函式呼叫引數時,會使用相同的指定規則

ts
interface Pet {
name: string;
}
let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
function greet(pet: Pet) {
console.log("Hello, " + pet.name);
}
greet(dog); // OK

請注意,dog 有額外的 owner 屬性,但這不會產生錯誤。檢查相容性時,只會考慮目標類型(此例中為 Pet)的成員。此比較程序會遞迴進行,探索每個成員和子成員的類型。

但是,請注意,物件文字 只能指定已知的屬性。例如,由於我們已明確指定 dog 的類型為 Pet,因此以下程式碼無效

ts
let dog: Pet = { name: "Lassie", owner: "Rudd Weatherwax" }; // Error

比較兩個函式

比較原始型別和物件型別相對簡單,但哪些類型的函式應被視為相容的問題則比較複雜。讓我們從兩個僅在參數清單中不同的函式的基本範例開始

ts
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error

若要檢查 x 是否可指定給 y,我們首先檢視參數清單。x 中的每個參數都必須在 y 中有對應的參數,且具有相容的型別。請注意,不會考慮參數的名稱,只會考慮其型別。在此情況下,x 的每個參數在 y 中都有對應的相容參數,因此允許指定。

第二次指定會產生錯誤,因為 yx 沒有的第二個必要參數,因此不允許指定。

您可能會想知道為什麼我們允許「捨棄」參數,就像在 y = x 的範例中。允許此指定的原因是,在 JavaScript 中忽略額外的函式參數實際上很常見。例如,Array#forEach 會提供三個參數給 callback 函式:陣列元素、其索引和包含的陣列。儘管如此,只使用第一個參數來提供 callback 函式非常有用

ts
let items = [1, 2, 3];
// Don't force these extra parameters
items.forEach((item, index, array) => console.log(item));
// Should be OK!
items.forEach((item) => console.log(item));

現在讓我們來看看回傳型別如何處理,使用兩個僅回傳型別不同的函式

ts
let x = () => ({ name: "Alice" });
let y = () => ({ name: "Alice", location: "Seattle" });
x = y; // OK
y = x; // Error, because x() lacks a location property

型別系統強制要求來源函式的回傳型別是目標型別回傳型別的子型別。

函數參數雙變性

當比較函數參數的類型時,如果來源參數可以指定給目標參數,或反之亦然,則指定會成功。這是不可靠的,因為呼叫者可能會得到一個採用更專業類型的函數,但使用較不專業的類型呼叫該函數。實際上,這種錯誤很少見,而允許這樣做可以啟用許多常見的 JavaScript 模式。一個簡短的範例

ts
enum EventType {
Mouse,
Keyboard,
}
interface Event {
timestamp: number;
}
interface MyMouseEvent extends Event {
x: number;
y: number;
}
interface MyKeyEvent extends Event {
keyCode: number;
}
function listenEvent(eventType: EventType, handler: (n: Event) => void) {
/* ... */
}
// Unsound, but useful and common
listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x + "," + e.y));
// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) =>
console.log((e as MyMouseEvent).x + "," + (e as MyMouseEvent).y)
);
listenEvent(EventType.Mouse, ((e: MyMouseEvent) =>
console.log(e.x + "," + e.y)) as (e: Event) => void);
// Still disallowed (clear error). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e));

你可以透過編譯器標記 strictFunctionTypes,讓 TypeScript 在發生這種情況時引發錯誤。

可選參數和剩餘參數

在比較函數的相容性時,可選參數和必要參數是可以互換的。來源類型的額外可選參數並非錯誤,而目標類型中沒有對應來源類型參數的可選參數也並非錯誤。

當函數有一個 rest 參數時,它會被視為一連串無限的選用參數。

從類型系統的角度來看,這是不健全的,但從執行時間的角度來看,選用參數的概念通常不會被嚴格執行,因為在該位置傳遞 undefined 對大多數函數來說是等效的。

激勵範例是一個函數的常見模式,它會接收一個回呼並使用一些可預測(對程式設計師來說)但未知(對類型系統來說)數量的參數來呼叫它。

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

具有重載的函數

當函數具有重載時,目標類型中的每個重載都必須與來源類型上的相容簽章相符。這可確保來源函數可以在與目標函數相同的所有情況下被呼叫。

列舉

列舉相容於數字,而數字也相容於列舉。不同列舉類型的列舉值被視為不相容。例如:

ts
enum Status {
Ready,
Waiting,
}
enum Color {
Red,
Blue,
Green,
}
let status = Status.Ready;
status = Color.Green; // Error

類別

類別的工作方式類似於物件文字類型和介面,只有一個例外:它們同時具有靜態和實例類型。在比較兩個類別類型的物件時,只會比較實例的成員。靜態成員和建構函式不會影響相容性。

ts
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

類別中的私有和受保護成員

類別中的私有和受保護成員會影響其相容性。當檢查類別實例的相容性時,如果目標類型包含私有成員,則來源類型也必須包含源自同一個類別的私有成員。同樣地,受保護成員的實例也適用相同的規則。這允許類別與其超類別具有指派相容性,但與來自不同繼承層級且具有相同形狀的類別相容。

泛型

由於 TypeScript 是一種結構化類型系統,因此類型參數僅在作為成員類型的一部分使用時才會影響結果類型。例如,

ts
interface Empty<T> {}
let x: Empty<number>;
let y: Empty<string>;
x = y; // OK, because y matches structure of x

在上面,xy 相容,因為它們的結構沒有以差異化的方式使用類型參數。透過新增一個成員到 Empty<T> 來變更此範例,顯示它是如何運作的

ts
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
x = y; // Error, because x and y are not compatible

以這種方式,指定其類型參數的泛型類型就像非泛型類型一樣運作。

對於沒有指定類型參數的泛型類型,相容性會透過在所有未指定的類型參數中指定 any 來檢查。然後檢查結果類型是否相容,就像在非泛型案例中一樣。

例如,

ts
let identity = function <T>(x: T): T {
// ...
};
let reverse = function <U>(y: U): U {
// ...
};
identity = reverse; // OK, because (x: any) => any matches (y: any) => any

進階主題

子類型與指定

到目前為止,我們使用了「相容」,這不是語言規範中定義的術語。在 TypeScript 中,有兩種相容性:子類型和指定。它們之間的差異僅在於指定會將子類型相容性擴充套件為允許指定到和從 any,以及指定到和從具有對應數值的值的 enum 的規則。

語言中的不同位置會根據情況使用這兩種相容性機制之一。在實務上,類型相容性是由指定相容性決定的,即使在 implementsextends 子句的情況下也是如此。

anyunknownobjectvoidundefinednullnever 可指派性

下表總結了一些抽象類型之間的可指派性。列表示每個類型的可指派目標,行表示可指派給它們的類型。當 strictNullChecks 關閉時,一個「」表示一個僅相容的組合。

any unknown object void undefined null never
any →
unknown →
object →
void →
undefined →
null →
never →

重申 基礎知識

  • 所有內容都可指派給它自己。
  • anyunknown 在可指派給它們的內容方面相同,不同之處在於 unknown 不可指派給除了 any 之外的任何內容。
  • unknownnever 就像彼此的逆數。所有內容都可指派給 unknownnever 可指派給所有內容。沒有任何內容可指派給 neverunknown 不可指派給任何內容(除了 any)。
  • void 無法指定為或從任何東西,但有以下例外:anyunknownneverundefinednull(如果 strictNullChecks 已關閉,請參閱表格以取得詳細資料)。
  • strictNullChecks 已關閉時,nullundefined 類似於 never:可指定為大多數類型,大多數類型無法指定為它們。它們可以互相指定。
  • strictNullChecks 已開啟時,nullundefined 的行為更類似於 void:無法指定為或從任何東西,但 anyunknownnevervoid 除外(undefined 永遠可以指定為 void)。

TypeScript 文件是一個開放原始碼專案。透過 傳送拉取請求 ❤ 來協助我們改善這些頁面。

此頁面的貢獻者
RCRyan Cavanaugh (51)
DRDaniel Rosenwasser (19)
OTOrta Therox (18)
MHMohamed Hegazy (4)
JBJack Bates (3)
25+

最後更新時間:2024 年 3 月 21 日