TypeScript – 物件與介面(Objects & Interfaces)
主題:interface vs type 的差異
簡介
在日常的 TypeScript 開發中,interface 與 type 幾乎同時出現在型別宣告裡。
對於剛踏入 TypeScript 的開發者而言,兩者的語法相似度讓人容易混淆,甚至會錯誤地認為它們可以完全互換。
事實上,雖然兩者都能描述物件的結構,但在 擴充性、合併行為、可表達的型別範圍 以及 編譯器的錯誤訊息 上仍有不少差異。了解這些差異不僅能寫出更具可讀性與可維護性的程式碼,也能在大型專案中避免不必要的型別衝突。
本文將從概念、語法、實務範例、常見陷阱與最佳實踐等角度,完整說明 interface 與 type 的差別,讓讀者在選擇使用時能夠依情境作出最合適的決策。
核心概念
1. 基本宣告方式
| 方式 | 語法 | 用途 |
|---|---|---|
interface |
interface Person { name: string; age: number; } |
用來描述 物件形狀、類別實作、函式簽名 等。 |
type |
type Person = { name: string; age: number; }; |
可以描述 任意型別(聯合、交叉、條件型別等),不僅限於物件。 |
重點:
type是 型別別名(type alias),它可以把任何 TypeScript 型別(包括原始型別、聯合型別、交叉型別…)賦予一個易讀的名稱;而interface僅能描述 物件型別(或函式型別的呼叫簽名)。
2. 擴充與合併(Declaration Merging)
interface 的自動合併
interface User {
id: number;
name: string;
}
// 另一個檔案或同一檔案再次宣告同名的 interface
interface User {
email?: string;
}
編譯結果等同於:
interface User {
id: number;
name: string;
email?: string;
}
說明:只要名稱相同,所有
interface會自動 合併(declaration merging),這在擴充第三方套件的型別或逐步加入新屬性時非常便利。
type 不會合併
type User = {
id: number;
name: string;
};
type User = {
email?: string; // ❌ 會產生重複定義錯誤
};
若想要擴充 type,只能使用 交叉型別(&):
type UserBase = { id: number; name: string };
type UserExtended = UserBase & { email?: string };
3. 可表達的型別範圍
| 功能 | interface |
type |
|---|---|---|
| 物件結構 | ✅ | ✅ |
| 函式簽名(呼叫簽名) | ✅ | ✅ |
交叉型別 (&) |
❌(只能透過 extends) |
✅ |
| 聯合型別 (` | `) | ❌ |
條件型別 (T extends U ? X : Y) |
❌ | ✅ |
內建映射型別 (Partial<T>, Pick<T, K>) |
❌(需要先有型別別名) | ✅ |
具名的元組 ([string, number]) |
❌ | ✅ |
結論:如果需要 聯合、交叉、條件、映射 等進階型別功能,
type是唯一的選擇;若僅描述物件形狀且希望利用自動合併特性,interface更為合適。
4. 可擴充性(Extending)
interface 繼承
interface Animal {
species: string;
}
interface Dog extends Animal {
bark(): void;
}
type 交叉擴充
type Animal = { species: string };
type Dog = Animal & {
bark(): void;
};
兩者在語意上相同,但 interface 支援多重繼承(extends A, B, C),而 type 只能透過多個 & 實現同樣效果。
程式碼範例
以下示範 5 個實用案例,說明在不同情境下選擇 interface 或 type 的原因。
範例 1:基本物件描述(兩者皆可)
// 使用 interface
interface Point {
x: number;
y: number;
}
// 使用 type
type PointAlias = {
x: number;
y: number;
};
const p1: Point = { x: 10, y: 20 };
const p2: PointAlias = { x: 5, y: 15 };
提示:若未預期會被其他模組「合併」或「擴充」,兩者等價,依團隊慣例選擇即可。
範例 2:函式呼叫簽名
// interface 版
interface Comparator {
(a: number, b: number): number;
}
// type 版
type ComparatorAlias = (a: number, b: number) => number;
const asc: Comparator = (a, b) => a - b;
const desc: ComparatorAlias = (a, b) => b - a;
說明:兩者都能描述函式型別,但
interface允許同時加入屬性(如description?: string),而type則只能是純函式或交叉型別。
範例 3:擴充第三方型別(Declaration Merging)
// 假設第三方套件提供以下介面
declare module "axios" {
interface AxiosRequestConfig {
timeout?: number;
}
}
// 我們想加上自訂屬性
declare module "axios" {
interface AxiosRequestConfig {
retry?: number; // ✅ 直接合併
}
}
重點:若改用
type,就必須重新定義整個型別,無法像interface那樣「自動」合併。
範例 4:聯合與條件型別(只能用 type)
type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; error: string };
type ExtractSuccess<T> = T extends { status: "success"; data: infer D } ? D : never;
// 使用
type UserRes = ApiResponse<{ id: number; name: string }>;
type SuccessData = ExtractSuccess<UserRes>; // { id: number; name: string }
說明:此類高度抽象的型別只能透過
type來實作,interface完全無法表達。
範例 5:映射型別與實用工具型別
type User = {
id: number;
name: string;
email?: string;
};
// 產生只讀版
type ReadonlyUser = Readonly<User>;
// 產生部分必填版
type PartialUser = Partial<User>;
// 結合 interface 與 utility type
interface IUser extends ReadonlyUser {} // ✅ 仍可擴充
小技巧:如果你已經有
type,仍可使用interface來 擴充(如上例),結合兩者的優點。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
重複宣告 type |
type 不能被合併,重複宣告會拋錯。 |
使用交叉型別或改用 interface。 |
過度使用 any |
為了省事直接寫 type Any = any,失去型別保護。 |
儘量使用具體結構或 unknown 再逐步縮小。 |
混用 interface & type 造成混淆 |
同一概念同時用兩種寫法,團隊閱讀成本升高。 | 依專案風格統一:物件型別用 interface,高階型別用 type。 |
遺忘 readonly / ? |
在 interface 中忘記加 readonly,導致意外修改。 |
使用 Readonly<T> 或在 interface 中明確標註。 |
| 錯誤的擴充順序 | 先用 type 再 interface 合併,會失去 type 的特性。 |
先決定主體是 interface 或 type,再根據需求選擇擴充方式。 |
最佳實踐
物件型別首選
interface- 需要 自動合併(例如擴充第三方套件)時。
- 想讓類別
implements更直觀。
高階型別使用
type- 需要 聯合 / 交叉 / 條件 型別時。
- 想利用 TypeScript 提供的 utility types(
Partial、Pick、Exclude…)。
保持一致的命名慣例
I前綴(如IUser)在某些團隊中仍被接受,但在大型專案裡,直接使用User(interface)較為簡潔。- 若同時有
interface與type,可在檔案註解說明「此檔案主要使用interface描述 API 回傳結構」等。
盡量避免過度嵌套
- 讓型別保持扁平,便於讀取與維護。
- 使用
Pick<T, K>、Omit<T, K>抽取子集合,而不是手寫大量重複屬性。
實際應用場景
| 場景 | 建議使用 | 為什麼 |
|---|---|---|
第三方套件的型別擴充(如 axios、express) |
interface |
可以直接 declaration merging,不必重新定義整個型別。 |
| REST API 回傳的通用包裝(成功/失敗) | type |
需要 聯合型別 與 條件型別 來抽取成功資料。 |
| React Props / State | interface(或 type) |
大多數 UI 團隊習慣 interface,且可利用合併擴充元件的 Props。 |
資料模型的映射與轉換(如 Pick、Omit) |
type |
必須使用 utility type,而 interface 只能透過擴充或交叉實作。 |
| 大型企業內部 SDK | 兩者混用 | 基礎結構使用 interface,高階抽象(如 Result<T>)使用 type。 |
總結
interface專注於物件型別,提供 自動合併、多重繼承 的便利,特別適合 擴充第三方型別 以及 類別實作。type是 型別別名,能表示 任意型別(聯合、交叉、條件、映射),在 高階型別抽象 時不可或缺。- 在實務開發中,依需求選擇:若只需要描述結構且可能會被其他檔案擴充,首選
interface;若需要複雜的型別運算或工具型別,則使用type。 - 保持團隊風格的一致性、避免重複宣告、善用 TypeScript 提供的 utility types,才能讓程式碼既安全又易於維護。
掌握這兩者的差異與適用時機,將讓你的 TypeScript 專案在型別安全、可讀性與擴充性上都有顯著提升。祝開發順利! 🚀