本文 AI 產出,尚未審核

TypeScript – 物件與介面(Objects & Interfaces)

主題:interface vs type 的差異


簡介

在日常的 TypeScript 開發中,interfacetype 幾乎同時出現在型別宣告裡。
對於剛踏入 TypeScript 的開發者而言,兩者的語法相似度讓人容易混淆,甚至會錯誤地認為它們可以完全互換。
事實上,雖然兩者都能描述物件的結構,但在 擴充性、合併行為、可表達的型別範圍 以及 編譯器的錯誤訊息 上仍有不少差異。了解這些差異不僅能寫出更具可讀性與可維護性的程式碼,也能在大型專案中避免不必要的型別衝突。

本文將從概念、語法、實務範例、常見陷阱與最佳實踐等角度,完整說明 interfacetype 的差別,讓讀者在選擇使用時能夠依情境作出最合適的決策。


核心概念

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 個實用案例,說明在不同情境下選擇 interfacetype 的原因。

範例 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 中明確標註。
錯誤的擴充順序 先用 typeinterface 合併,會失去 type 的特性。 先決定主體是 interfacetype,再根據需求選擇擴充方式。

最佳實踐

  1. 物件型別首選 interface

    • 需要 自動合併(例如擴充第三方套件)時。
    • 想讓類別 implements 更直觀。
  2. 高階型別使用 type

    • 需要 聯合 / 交叉 / 條件 型別時。
    • 想利用 TypeScript 提供的 utility typesPartialPickExclude…)。
  3. 保持一致的命名慣例

    • I 前綴(如 IUser)在某些團隊中仍被接受,但在大型專案裡,直接使用 User(interface)較為簡潔。
    • 若同時有 interfacetype,可在檔案註解說明「此檔案主要使用 interface 描述 API 回傳結構」等。
  4. 盡量避免過度嵌套

    • 讓型別保持扁平,便於讀取與維護。
    • 使用 Pick<T, K>Omit<T, K> 抽取子集合,而不是手寫大量重複屬性。

實際應用場景

場景 建議使用 為什麼
第三方套件的型別擴充(如 axiosexpress interface 可以直接 declaration merging,不必重新定義整個型別。
REST API 回傳的通用包裝(成功/失敗) type 需要 聯合型別條件型別 來抽取成功資料。
React Props / State interface(或 type 大多數 UI 團隊習慣 interface,且可利用合併擴充元件的 Props。
資料模型的映射與轉換(如 PickOmit type 必須使用 utility type,而 interface 只能透過擴充或交叉實作。
大型企業內部 SDK 兩者混用 基礎結構使用 interface,高階抽象(如 Result<T>)使用 type

總結

  • interface 專注於物件型別,提供 自動合併多重繼承 的便利,特別適合 擴充第三方型別 以及 類別實作
  • type型別別名,能表示 任意型別(聯合、交叉、條件、映射),在 高階型別抽象 時不可或缺。
  • 在實務開發中,依需求選擇:若只需要描述結構且可能會被其他檔案擴充,首選 interface;若需要複雜的型別運算或工具型別,則使用 type
  • 保持團隊風格的一致性、避免重複宣告、善用 TypeScript 提供的 utility types,才能讓程式碼既安全又易於維護。

掌握這兩者的差異與適用時機,將讓你的 TypeScript 專案在型別安全、可讀性與擴充性上都有顯著提升。祝開發順利! 🚀