TypeScript 中的類型相容性基於結構化子類型。結構化類型是一種僅根據成員關聯類型的途徑。這與名義類型形成對比。考慮以下程式碼
ts
interface Pet {name: string;}class Dog {name: string;}let pet: Pet;// OK, because of structural typingpet = new Dog();
在像 C# 或 Java 等名義類型語言中,等效程式碼會是一個錯誤,因為 Dog
類別並未明確描述自己是 Pet
介面的實作方。
TypeScript 的結構化類型系統是根據 JavaScript 程式碼的典型寫法而設計的。由於 JavaScript 廣泛使用匿名物件,例如函式表達式和物件文字,因此使用結構化類型系統而不是名義類型系統來表示 JavaScript 函式庫中找到的關係類型更加自然。
健全性注意事項
TypeScript 的類型系統允許某些作業在編譯時無法得知為安全。當類型系統具有此屬性時,表示它「不健全」。TypeScript 允許不健全行為的地方經過仔細考量,我們將在本文檔中說明這些情況發生的位置及其背後的動機情境。
開始
TypeScript 結構類型系統的基本規則是,如果 y
至少具有與 x
相同的成員,則 x
與 y
相容。例如,考慮以下涉及名為 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; // OKx = y; // Error
若要檢查 x
是否可指定給 y
,我們首先檢視參數清單。x
中的每個參數都必須在 y
中有對應的參數,且具有相容的型別。請注意,不會考慮參數的名稱,只會考慮其型別。在此情況下,x
的每個參數在 y
中都有對應的相容參數,因此允許指定。
第二次指定會產生錯誤,因為 y
有 x
沒有的第二個必要參數,因此不允許指定。
您可能會想知道為什麼我們允許「捨棄」參數,就像在 y = x
的範例中。允許此指定的原因是,在 JavaScript 中忽略額外的函式參數實際上很常見。例如,Array#forEach
會提供三個參數給 callback 函式:陣列元素、其索引和包含的陣列。儘管如此,只使用第一個參數來提供 callback 函式非常有用
ts
let items = [1, 2, 3];// Don't force these extra parametersitems.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; // OKy = 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 commonlistenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x + "," + e.y));// Undesirable alternatives in presence of soundnesslistenEvent(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 typeslistenEvent(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 argumentsinvokeLater([1, 2], (x, y) => console.log(x + ", " + y));// Confusing (x and y are actually required) and undiscoverableinvokeLater([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; // OKs = a; // OK
類別中的私有和受保護成員
類別中的私有和受保護成員會影響其相容性。當檢查類別實例的相容性時,如果目標類型包含私有成員,則來源類型也必須包含源自同一個類別的私有成員。同樣地,受保護成員的實例也適用相同的規則。這允許類別與其超類別具有指派相容性,但不與來自不同繼承層級且具有相同形狀的類別相容。
泛型
由於 TypeScript 是一種結構化類型系統,因此類型參數僅在作為成員類型的一部分使用時才會影響結果類型。例如,
ts
interface Empty<T> {}let x: Empty<number>;let y: Empty<string>;x = y; // OK, because y matches structure of x
在上面,x
和 y
相容,因為它們的結構沒有以差異化的方式使用類型參數。透過新增一個成員到 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
的規則。
語言中的不同位置會根據情況使用這兩種相容性機制之一。在實務上,類型相容性是由指定相容性決定的,即使在 implements
和 extends
子句的情況下也是如此。
any
、unknown
、object
、void
、undefined
、null
和 never
可指派性
下表總結了一些抽象類型之間的可指派性。列表示每個類型的可指派目標,行表示可指派給它們的類型。當 strictNullChecks
關閉時,一個「✓」表示一個僅相容的組合。
any | unknown | object | void | undefined | null | never | |
---|---|---|---|---|---|---|---|
any → | ✓ | ✓ | ✓ | ✓ | ✓ | ✕ | |
unknown → | ✓ | ✕ | ✕ | ✕ | ✕ | ✕ | |
object → | ✓ | ✓ | ✕ | ✕ | ✕ | ✕ | |
void → | ✓ | ✓ | ✕ | ✕ | ✕ | ✕ | |
undefined → | ✓ | ✓ | ✓ | ✓ | ✓ | ✕ | |
null → | ✓ | ✓ | ✓ | ✓ | ✓ | ✕ | |
never → | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
重申 基礎知識
- 所有內容都可指派給它自己。
any
和unknown
在可指派給它們的內容方面相同,不同之處在於unknown
不可指派給除了any
之外的任何內容。unknown
和never
就像彼此的逆數。所有內容都可指派給unknown
,never
可指派給所有內容。沒有任何內容可指派給never
,unknown
不可指派給任何內容(除了any
)。void
無法指定為或從任何東西,但有以下例外:any
、unknown
、never
、undefined
和null
(如果strictNullChecks
已關閉,請參閱表格以取得詳細資料)。- 當
strictNullChecks
已關閉時,null
和undefined
類似於never
:可指定為大多數類型,大多數類型無法指定為它們。它們可以互相指定。 - 當
strictNullChecks
已開啟時,null
和undefined
的行為更類似於void
:無法指定為或從任何東西,但any
、unknown
、never
和void
除外(undefined
永遠可以指定為void
)。