此頁面已棄用

此手冊頁面已更換,前往新頁面

介面

TypeScript 的核心原則之一是類型檢查著重於值的「形狀」。這有時稱為「鴨子型別」或「結構化子型別」。在 TypeScript 中,介面扮演命名這些型別的角色,而且是定義程式碼中合約以及與專案外部程式碼合約的強大方式。

我們的首個介面

查看介面運作方式最簡單的方法是從一個簡單範例開始

ts
function printLabel(labeledObj: { label: string }) {
console.log(labeledObj.label);
}
 
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
Try

類型檢查器會檢查對 printLabel 的呼叫。printLabel 函數有一個單一參數,要求傳入的物件有一個稱為 label 的屬性,類型為 string。請注意,我們的物件實際上具有比這更多的屬性,但編譯器只會檢查至少存在所需屬性,且與所需的類型相符。在某些情況下,TypeScript 沒有這麼寬容,我們稍後會說明。

我們可以再次撰寫相同的範例,這次使用介面來描述具有字串 label 屬性的需求

ts
interface LabeledValue {
label: string;
}
 
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}
 
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
Try

介面 LabeledValue 是我們現在可以使用的一個名稱,用來描述前一個範例中的需求。它仍然表示具有稱為 label 的單一屬性,類型為 string。請注意,我們不必明確指出傳遞給 printLabel 的物件實作這個介面,就像我們在其他語言中可能必須做的那樣。在這裡,只有形狀才有關係。如果傳遞給函數的物件符合列出的需求,則允許傳遞。

值得指出的是,類型檢查器不要求這些屬性以任何順序出現,只要介面要求的屬性存在且具有所需的類型即可。

選用屬性

介面的所有屬性可能並非都是必要的。有些屬性只在特定條件下存在,或可能根本不存在。這些選用屬性在建立「選項包」等模式時很常見,您會將一個物件傳遞給只填入幾個屬性的函式。

以下為此模式的一個範例

ts
interface SquareConfig {
color?: string;
width?: number;
}
 
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = { color: "white", area: 100 };
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
 
let mySquare = createSquare({ color: "black" });
Try

具有選用屬性的介面寫法類似於其他介面,每個選用屬性在宣告中都會在屬性名稱尾端標示 ?

選用屬性的優點是,您可以描述這些可能存在的屬性,同時還能防止使用不屬於介面的屬性。例如,如果我們在 createSquare 中輸入錯誤的 color 屬性名稱,我們會收到一則錯誤訊息,告知我們

ts
interface SquareConfig {
color?: string;
width?: number;
}
 
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = { color: "white", area: 100 };
if (config.clor) {
Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?2551Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?
// Error: Property 'clor' does not exist on type 'SquareConfig'
newSquare.color = config.clor;
Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?2551Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
 
let mySquare = createSquare({ color: "black" });
Try

唯讀屬性

有些屬性應僅在物件第一次建立時才能修改。您可以透過在屬性名稱之前加上 readonly 來指定這項設定

ts
interface Point {
readonly x: number;
readonly y: number;
}
Try

您可以透過指定物件文字來建構 Point。指定後,xy 就無法變更。

ts
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
Cannot assign to 'x' because it is a read-only property.2540Cannot assign to 'x' because it is a read-only property.
Try

TypeScript 附帶 ReadonlyArray<T> 類型,它與 Array<T> 相同,但已移除所有變更方法,因此您可以確保在建立後不會變更陣列

ts
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
 
ro[0] = 12; // error!
Index signature in type 'readonly number[]' only permits reading.2542Index signature in type 'readonly number[]' only permits reading.
ro.push(5); // error!
Property 'push' does not exist on type 'readonly number[]'.2339Property 'push' does not exist on type 'readonly number[]'.
ro.length = 100; // error!
Cannot assign to 'length' because it is a read-only property.2540Cannot assign to 'length' because it is a read-only property.
a = ro; // error!
The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.4104The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
Try

在程式碼片段的最後一行,您可以看到,即使將整個 ReadonlyArray 指定回一般陣列也是不允許的。不過,您仍可透過類型斷言來覆寫它

ts
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
 
a = ro as number[];
Try

readonlyconst

要記住何時使用 readonlyconst 最簡單的方法是,詢問您是在變數或屬性上使用它。變數使用 const,而屬性使用 readonly

多餘屬性檢查

在我們使用介面的第一個範例中,TypeScript 讓我們傳遞 { size: number; label: string; } 給只預期 { label: string; } 的東西。我們也剛學到選用屬性,以及它們在描述所謂的「選項包」時有多麼有用。

然而,天真地結合這兩個會讓錯誤有機可乘。例如,使用 createSquare 的最後一個範例

ts
interface SquareConfig {
color?: string;
width?: number;
}
 
function createSquare(config: SquareConfig): { color: string; area: number } {
return {
color: config.color || "red",
area: config.width ? config.width * config.width : 20,
};
}
 
let mySquare = createSquare({ colour: "red", width: 100 });
Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'. Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?2345Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'. Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
Try

請注意傳遞給 createSquare 的參數拼寫成 colour 而不是 color。在純粹的 JavaScript 中,這種事情會靜默失敗。

你可以爭辯這個程式正確地輸入,因為 width 屬性相容,沒有出現 color 屬性,而且多餘的 colour 屬性微不足道。

然而,TypeScript 採取的立場是這段程式碼中可能有一個錯誤。當將物件文字指派給其他變數或將它們傳遞為參數時,它們會獲得特殊處理並進行多餘屬性檢查。如果物件文字有任何「目標類型」沒有的屬性,你會收到錯誤訊息

ts
let mySquare = createSquare({ colour: "red", width: 100 });
Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'. Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?2345Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'. Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
Try

繞過這些檢查其實很簡單。最簡單的方法就是使用類型斷言

ts
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
Try

不過,如果你確定物件可以有額外的屬性,並以特殊的方式使用,更好的方法可能是新增字串索引簽章。如果 SquareConfig 可以有上述類型的 colorwidth 屬性,但也可以有其他任何數量的屬性,那麼我們可以這樣定義

ts
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
Try

我們稍後會討論索引簽章,但這裡我們說的是 SquareConfig 可以有任意數量的屬性,只要它們不是 colorwidth,它們的類型就不重要。

繞過這些檢查的最後一種方法可能會有點令人驚訝,那就是將物件指定給另一個變數:由於 squareOptions 沒有經過過多屬性檢查,因此編譯器不會產生錯誤。

ts
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
Try

只要 squareOptionsSquareConfig 之間有共用屬性,上述解決方法就會有效。在此範例中,屬性是 width。不過,如果變數沒有任何共用物件屬性,就會失敗。例如

ts
let squareOptions = { colour: "red" };
let mySquare = createSquare(squareOptions);
Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.2559Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.
Try

請記住,對於上述簡單的程式碼,你可能不應該嘗試「繞過」這些檢查。對於具有方法和狀態的更複雜物件文字,你可能需要記住這些技巧,但大多數過多屬性錯誤實際上都是錯誤。這表示如果你遇到選項袋等內容的過多屬性檢查問題,你可能需要修改一些類型宣告。在此範例中,如果將具有 colorcolour 屬性的物件傳遞給 createSquare 是可以的,你應該修正 SquareConfig 的定義以反映這一點。

函數類型

介面能夠描述 JavaScript 物件可以採取的各種形狀。除了使用屬性描述物件外,介面也能夠描述函數類型。

若要使用介面描述函數類型,我們會給介面一個呼叫簽章。這就像函數宣告,只給出參數清單和回傳類型。參數清單中的每個參數都需要名稱和類型。

ts
interface SearchFunc {
(source: string, subString: string): boolean;
}
Try

定義好之後,我們可以使用這個函數類型介面,就像使用其他介面一樣。這裡,我們展示如何建立函數類型的變數,並指派給它同類型的函數值。

ts
let mySearch: SearchFunc;
 
mySearch = function (source: string, subString: string): boolean {
let result = source.search(subString);
return result > -1;
};
Try

若要讓函數類型正確地進行類型檢查,參數的名稱不需要相符。例如,我們可以像這樣撰寫上述範例

ts
let mySearch: SearchFunc;
 
mySearch = function (src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
};
Try

函數參數會逐一檢查,將每個對應參數位置的類型彼此檢查。如果你完全不想指定類型,TypeScript 的內容類型可以推論出引數類型,因為函數值直接指派給類型為 SearchFunc 的變數。這裡,我們的函數表達式的回傳類型也由它回傳的值暗示 (這裡是 falsetrue)。

ts
let mySearch: SearchFunc;
 
mySearch = function (src, sub) {
let result = src.search(sub);
return result > -1;
};
Try

如果函數表達式回傳數字或字串,類型檢查器會產生錯誤,指出回傳類型與 SearchFunc 介面中描述的回傳類型不相符。

ts
let mySearch: SearchFunc;
 
mySearch = function (src, sub) {
Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'. Type 'string' is not assignable to type 'boolean'.2322Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'. Type 'string' is not assignable to type 'boolean'.
let result = src.search(sub);
return "string";
};
Try

可索引類型

類似於我們如何使用介面來描述函式類型,我們也可以描述我們可以「索引」的類型,例如 a[10]ageMap["daniel"]。可索引類型有一個索引簽章,它描述了我們可以用來索引物件的類型,以及索引時的對應回傳類型。

讓我們舉個例子

ts
interface StringArray {
[index: number]: string;
}
 
let myArray: StringArray;
myArray = ["Bob", "Fred"];
 
let myStr: string = myArray[0];
Try

在上面,我們有一個具有索引簽章的 StringArray 介面。此索引簽章表示當 StringArray 使用 number 索引時,它將回傳一個 string

有四種類型的受支援索引簽章:字串、數字、符號和樣板字串。可以支援許多類型的索引器,但從數字索引器回傳的類型必須是從字串索引器回傳的類型的子類型。

這是因為在使用 number 索引時,JavaScript 實際上會在索引物件之前將其轉換為 string。這表示使用 100(一個 number)索引與使用 "100"(一個 string)索引是相同的事情,因此兩者需要一致。

ts
interface Animal {
name: string;
}
 
interface Dog extends Animal {
breed: string;
}
 
// Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
[x: number]: Animal;
'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.2413'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.
[x: string]: Dog;
}
Try

雖然字串索引簽章是描述「字典」模式的有力方式,但它們也強制所有屬性都符合其回傳類型。這是因為字串索引宣告 obj.property 也可用作 obj["property"]。在以下範例中,name 的類型與字串索引的類型不符,因此類型檢查器會產生錯誤

ts
interface NumberDictionary {
[index: string]: number;
 
length: number; // ok, length is a number
name: string; // error, the type of 'name' is not a subtype of the indexer
Property 'name' of type 'string' is not assignable to 'string' index type 'number'.2411Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
}
Try

但是,如果索引簽章是屬性類型的聯集,則不同類型的屬性是可以接受的

ts
interface NumberOrStringDictionary {
[index: string]: number | string;
 
length: number; // ok, length is a number
name: string; // ok, name is a string
}
Try

最後,你可以讓索引簽章為「唯讀」,以防止對其索引進行指定

ts
interface ReadonlyStringArray {
readonly [index: number]: string;
}
 
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
Index signature in type 'ReadonlyStringArray' only permits reading.2542Index signature in type 'ReadonlyStringArray' only permits reading.
Try

你無法設定「myArray[2]」,因為索引簽章為「唯讀」

使用範本字串的索引類型

範本字串可用於表示允許特定模式,但並非所有模式。例如,HTTP 標頭物件可能有一組已知的標頭,並支援任何以「x-」為字首的 自訂定義屬性

ts
interface HeadersResponse {
"content-type": string,
date: string,
"content-length": string
 
// Permit any property starting with 'x-'.
[headerName: `x-${string}`]: string;
}
 
function handleResponse(r: HeadersResponse) {
// Handle known, and x- prefixed
const type = r["content-type"]
const poweredBy = r["x-powered-by"]
 
// Unknown keys without the prefix raise errors
const origin = r.origin
Property 'origin' does not exist on type 'HeadersResponse'.2339Property 'origin' does not exist on type 'HeadersResponse'.
}
Try

類別類型

實作介面

在 C# 和 Java 等語言中,介面最常見的用途之一,就是明確強制類別符合特定合約,這在 TypeScript 中也是可行的。

ts
interface ClockInterface {
currentTime: Date;
}
 
class Clock implements ClockInterface {
currentTime: Date = new Date();
constructor(h: number, m: number) {}
}
Try

您還可以在介面中描述類別中實作的方法,就像我們在以下範例中使用 setTime 所做的那樣

ts
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
 
class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}
Try

介面描述類別的公開面,而不是公開面和私有面。這禁止您使用它們來檢查類別是否也有類別執行個體私有面的特定類型。

類別的靜態面和執行個體面的差異

在使用類別和介面時,請記住類別有兩個類型:靜態面的類型和執行個體面的類型。您可能會注意到,如果您建立一個具有建構簽章的介面,並嘗試建立一個實作此介面的類別,您會收到一個錯誤

ts
interface ClockConstructor {
new (hour: number, minute: number);
}
 
class Clock implements ClockConstructor {
Class 'Clock' incorrectly implements interface 'ClockConstructor'. Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.2420Class 'Clock' incorrectly implements interface 'ClockConstructor'. Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.
currentTime: Date;
constructor(h: number, m: number) {}
}
Try

這是因為當一個類別實作一個介面時,只會檢查類別的執行個體面。由於建構函式位於靜態面,因此不包含在此檢查中。

相反,您需要直接使用類別的靜態面。在此範例中,我們定義了兩個介面,ClockConstructor 用於建構函式,ClockInterface 用於執行個體方法。然後,為了方便,我們定義了一個建構函式函式 createClock,用來建立傳遞給它的類型的執行個體

ts
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
 
interface ClockInterface {
tick(): void;
}
 
function createClock(
ctor: ClockConstructor,
hour: number,
minute: number
): ClockInterface {
return new ctor(hour, minute);
}
 
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
}
 
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("tick tock");
}
}
 
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
Try

由於 createClock 的第一個參數是 ClockConstructor 類型,因此在 createClock(AnalogClock, 7, 32) 中,它會檢查 AnalogClock 是否有正確的建構函式簽章。

另一個簡單的方法是使用類別表達式

ts
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
 
interface ClockInterface {
tick(): void;
}
 
const Clock: ClockConstructor = class Clock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
};
 
let clock = new Clock(12, 17);
clock.tick();
Try

擴充介面

與類別一樣,介面也可以互相擴充。這允許您將一個介面的成員複製到另一個介面中,這讓您在將介面分隔成可重複使用的元件時有更大的彈性。

ts
interface Shape {
color: string;
}
 
interface Square extends Shape {
sideLength: number;
}
 
let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
Try

一個介面可以擴充多個介面,建立所有介面的組合。

ts
interface Shape {
color: string;
}
 
interface PenStroke {
penWidth: number;
}
 
interface Square extends Shape, PenStroke {
sideLength: number;
}
 
let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
Try

混合類型

正如我們先前提到的,介面可以描述實際 JavaScript 中存在的豐富類型。由於 JavaScript 的動態和彈性特性,您偶爾可能會遇到一個物件,它可以作為上面描述的某些類型的組合。

一個這樣的範例是一個物件,它同時作為一個函式和一個物件,並具有其他屬性

ts
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
 
function getCounter(): Counter {
let counter = function (start: number) {} as Counter;
counter.interval = 123;
counter.reset = function () {};
return counter;
}
 
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
Try

在與第三方 JavaScript 互動時,您可能需要使用上述模式來完整描述類型的形狀。

介面擴充類別

當介面類型延伸類別類型時,它會繼承類別的成員,但不會繼承其實作。這就像介面宣告了類別的所有成員,但沒有提供實作。介面甚至會繼承基底類別的私有和受保護成員。這表示當您建立延伸具有私有或受保護成員的類別的介面時,該介面類型只能由該類別或其子類別實作。

當您有龐大的繼承層級,但想要指定您的程式碼只適用於具有特定屬性的子類別時,這會很有用。除了繼承基底類別之外,子類別不需要有其他關聯。例如

ts
class Control {
private state: any;
}
 
interface SelectableControl extends Control {
select(): void;
}
 
class Button extends Control implements SelectableControl {
select() {}
}
 
class TextBox extends Control {
select() {}
}
 
class ImageControl implements SelectableControl {
Class 'ImageControl' incorrectly implements interface 'SelectableControl'. Types have separate declarations of a private property 'state'.2420Class 'ImageControl' incorrectly implements interface 'SelectableControl'. Types have separate declarations of a private property 'state'.
private state: any;
select() {}
}
Try

在上述範例中,SelectableControl 包含 Control 的所有成員,包括私有的 state 屬性。由於 state 是私有成員,因此只有 Control 的子代才能實作 SelectableControl。這是因為只有 Control 的子代才會具有源自相同宣告的 state 私有成員,而這是私有成員相容性的必要條件。

Control 類別中,可以透過 SelectableControl 的執行個體存取 state 私有成員。實際上,SelectableControl 會像已知具有 select 方法的 ControlButtonTextBox 類別是 SelectableControl 的子類型(因為它們都繼承自 Control,而且都具有 select 方法)。ImageControl 類別有自己的 state 私有成員,而不是延伸 Control,因此它無法實作 SelectableControl

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

此頁面的貢獻者
RCRyan Cavanaugh (55)
DRDaniel Rosenwasser (25)
OTOrta Therox (23)
Jjswheeler (3)
MHMohamed Hegazy (3)
44+

最後更新:2024 年 3 月 21 日