TypeScript 特性 - Interface
**interface** 用於定義一組預定義的屬性和方法,它是一種型別定義語法,可以用來檢查一個物件是否符合特定的屬性和方法。
interface 只存在於編譯時期(compile-time),編譯成 JavaScript 後會完全消失,不會產生任何 runtime 程式碼,因此沒有效能負擔。它的角色是「型別契約」:描述一個物件應該長什麼樣子,而不是建立一個值。
例如,可以定義一個介面表示音樂物件:
interface Music {
title: string;
artist: string;
genre: MusicType;
play(): void;
pause(): void;
}
interface vs classclass 同時是型別「也是」值(會編譯成 runtime 程式碼、可以 new);interface 純粹是型別。class 可以 implements 一個 interface 來保證自己符合契約。
進階用法
可選屬性與唯讀屬性
interface User {
readonly id: number; // 唯讀,建立後不可修改
name: string;
email?: string; // 可選屬性,型別是 string | undefined
}
const u: User = { id: 1, name: 'Bob' };
u.id = 2; // ❌ 編譯錯誤:Cannot assign to 'id' because it is a read-only property
u.email; // 型別為 string | undefined
兩個常見陷阱:
| 陷阱 | 說明 |
|---|---|
readonly 只防淺層 | readonly arr: number[] 仍可 arr.push(1);要完全唯讀用 readonly number[] 或 ReadonlyArray<number> |
? 不等於 | undefined | email?: string 可以整個省略 key;email: string | undefined 則必須寫出 key(只是值可以是 undefined) |
interface A { x?: number; }
interface B { x: number | undefined; }
const a: A = {}; // ✅ 可省略
const b: B = {}; // ❌ Property 'x' is missing
const b2: B = { x: undefined }; // ✅ 必須顯式給 undefined
繼承(Extends)
interface 可以用 extends 繼承一個或多個介面,子介面會擁有所有父介面的成員。
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
const myDog: Dog = { name: 'Buddy', breed: 'Golden Retriever' };
多重繼承(用逗號分隔):
interface Serializable { serialize(): string; }
interface Loggable { log(): void; }
interface Entity extends Serializable, Loggable {
id: number;
}
陷阱:繼承時若子介面重新宣告同名屬性,新型別必須能 assign 給父型別,否則報錯。
interface Base { value: string | number; }
interface Narrow extends Base { value: string; } // ✅ string 可賦值給 string | number
interface Bad extends Base { value: boolean; } // ❌ boolean 不相容於 string | number
interface 也能 extends 一個 type(只要該 type 是物件型別),反之 type 也能用 & 交集 interface,兩者可互通。
函式型別
interface 可描述「可被呼叫」的型別,把呼叫簽名(call signature)寫在介面裡:
interface SearchFunc {
(source: string, subString: string): boolean;
}
const mySearch: SearchFunc = (src, sub) => src.includes(sub);
參數名不需對應(src/sub 對 source/subString 無妨),TypeScript 只比對「位置 + 型別」。
也可以同時描述「函式 + 屬性」(hybrid type)或「可被 new 的建構簽名」:
interface Counter {
(start: number): string; // 呼叫簽名
interval: number; // 同時掛屬性
reset(): void;
}
interface ClockConstructor {
new (hour: number, minute: number): object; // 建構簽名,描述 class 本身
}
索引簽名(Index Signature)
當物件的 key 數量不固定、但型別一致時使用:
interface StringMap {
[key: string]: string;
}
const headers: StringMap = {
'Content-Type': 'application/json',
'Authorization': 'Bearer token',
};
關鍵規則 / 陷阱:
-
所有具名屬性的型別都必須相容於索引簽名的型別。下例會報錯,因為
length: number不符合[key: string]: string:interface Bad { [key: string]: string; length: number; // ❌ 'number' 不相容於 string index 的 'string' }解法是放寬索引型別:
[key: string]: string | number。 -
key 只能是
string、number、symbol或 template literal type。number索引的回傳型別必須是string索引回傳型別的子型別(因為 JS 物件 key 實際上都是字串)。 -
用 index signature 會失去 key 拼字檢查:
headers.contentType(打錯)不會報錯,會被當成合法存取回傳string。需要精確 key 時改用Record<K, V>或具名屬性。
宣告合併(Declaration Merging)
同名的多個 interface 會自動合併成一個,這是 interface 獨有、type 沒有的能力。
interface Box {
width: number;
}
interface Box {
height: number;
}
// 合併後等同於 { width: number; height: number; }
const b: Box = { width: 10, height: 20 };
合併規則陷阱:
-
非函式的同名屬性型別必須一致,否則報錯(
width: number又宣告width: string→ 衝突)。 -
同名方法會 overload 合併,後宣告的順序排在前面。
-
最常見的實務用途是擴充第三方 / 全域型別,例如替 Express 的
Request加自訂欄位,或擴充Window:// 擴充全域 Window declare global { interface Window { myAppConfig: { version: string }; } } window.myAppConfig.version; // ✅ 不再報錯
這也是「為什麼寫函式庫的公開型別常用 interface」的主因 —— 使用者可以透過 declaration merging 擴充你的型別,type alias 做不到。
interface vs type 的差異與選用
兩者高度重疊,多數情況可互換,但有幾個關鍵差異:
| 面向 | interface | type(type alias) |
|---|---|---|
| 宣告合併 | ✅ 同名自動合併 | ❌ 同名直接報「Duplicate identifier」 |
| 描述對象 | 只能是物件 / 函式 / class 形狀 | 任何型別:union、tuple、primitive、mapped、conditional… |
| 繼承語法 | extends(建立顯式階層,錯誤訊息較清楚) | & 交集(intersection) |
| union / 交集 | 不能直接定義 union | type T = A | B ✅ |
| 計算屬性 (mapped / conditional) | ❌ | ✅ type Keys = keyof T 等 |
| 效能(編譯器) | 快取友善,大型專案略快 | 複雜交集 / 條件型別可能較慢 |
| 錯誤訊息 | 通常顯示 interface 名稱,較易讀 | 複雜 type 常被展開成一大坨 |
只能用 type(interface 做不到)的場景:
type ID = string | number; // union
type Point = [number, number]; // tuple
type Name = string; // primitive 別名
type Nullable<T> = T | null; // 泛型 + union
type Keys = keyof SomeObject; // keyof
type ReadonlyUser = Readonly<User>; // mapped / utility type
只有 interface 做得到的場景:
- 宣告合併(擴充
Window、Express.Request、第三方.d.ts)。
選用準則(面試標準答法)
- 物件 / class 的「形狀」契約,且可能被外部擴充 → 用
interface(尤其是函式庫公開 API)。 - 需要 union、tuple、primitive 別名、mapped / conditional type → 用
type(interface 辦不到)。 - 團隊一致性優先:兩者能做的事大部分重疊,挑一個當預設、保持一致比糾結哪個更重要。常見約定是「物件用 interface、其餘用 type」。
常見誤解「interface 不能用 utility type / 不能被 Partial 包」是錯的。
interface 定義的型別一樣可以丟進 Partial<User>、Pick<User, 'name'>。差別只在「能不能直接寫出」union / mapped type,不影響它被其他型別運算消費。