TypeScript 介面與型別別名:建立自訂型別

2025年7月25日

💎 加入 E+ 成長計畫 與超過 700+ 位工程師一同在社群成長,並獲得更多深度的軟體前後端學習資源

在開發應用程式時,相信大家都對物件的資料結構不陌生。在先前的文章中,我們探討了 TypeScript 的基礎型別以及它們如何在編譯期間幫助開發者捕捉錯誤。現在,讓我們深入學習 TypeScript 中最強大的功能之一:建立自訂型別來描述複雜物件的結構。

假設我們有一個用來顯示文章預覽的函式,需要在不同元件間傳遞文章資料。一開始,這個實作在完整的測試資料環境下運作得很順利。

過了幾週,在開發新功能時,有個不完整的文章物件被傳入了顯示函式。當程式嘗試存取不存在的屬性時,在正式環境中發生了錯誤。

// 在完整測試資料下運作正常...
const createPreview = (article) => {
  return `${article.title} - ${article.content.substring(0, 100)}...`;
  // TypeError: Cannot read property 'substring' of undefined
};

// 某處的程式碼傳入了不完整的物件
const incompleteArticle = { title: "測試文章" }; // 缺少 content!
createPreview(incompleteArticle);

這種情況相信大家都遇過,而這正是 TypeScript 介面要解決的核心問題。在 JavaScript 中,物件可以隨時具有任何形狀,而且我們無法確知期望的結構是否與實際拿到的結構相符,直到真正使用時才會發現問題。

介面是形狀的約定

當程式碼因為嘗試存取不存在的屬性而崩潰時,這種挫折感相信大家都不陌生。就像上面的例子中,當 article.content 是 undefined 時就會發生錯誤。TypeScript 介面透過讓我們描述物件應該具備哪些屬性來解決這個問題。一旦定義了介面,TypeScript 就會檢查每個物件是否符合該描述,並在有遺漏時提醒我們。

可以把介面想像成一份檢查清單。當我們定義介面時,等於是建立了一份清單,說明「任何使用這個介面的物件都必須具備這些特定屬性」。接著 TypeScript 會對照這份清單檢查每個物件。

interface Article {
  id: number;
  title: string;
  content: string;
  publishedAt: string;
}

const createPreview = (article: Article) => {
  // TypeScript 保證 article 有 title、content 等屬性
  return `${article.title} - ${article.content.substring(0, 100)}...`;
};

現在當不完整的物件被傳入時,不會在執行期間崩潰。相反地,TypeScript 會在編輯器中顯示紅色波浪底線,並給出清楚的錯誤訊息:「Property 'content' is missing in type '{ title: string; }' but required in type 'Article'」。錯誤在開發階段就被捕捉到,而非在正式環境中才發現。

這樣的保護機制在我們使用 Article 介面的任何地方都會生效。不論是建立文章、顯示文章,或是在函式間傳遞文章,TypeScript 都會確保它們符合預期的結構。

結構重要,名稱不重要

TypeScript 有個一開始可能會讓人困惑的特點:它不在乎我們如何稱呼物件,只在乎物件的結構。

這被稱為結構化型別系統(structural typing),與 Java 或 C# 等使用名義型別系統(nominal typing)的語言不同。在名義型別系統中,物件必須明確宣告為特定型別才能相容。在結構化型別系統中,TypeScript 只會問:「這個物件有正確的結構嗎?」如果有,不管我們怎麼命名或如何建立它,都是相容的。

interface Article {
  title: string;
  content: string;
  publishedAt: string;
}

// 這個物件從未明確宣告為 Article
const blogPost = {
  title: "Understanding TypeScript",
  content: "TypeScript helps catch bugs...",
  publishedAt: "2025-01-15",
  category: "Programming", // 額外的屬性也沒問題
  views: 1250,
};

// 但它符合 Article 介面的結構
const article: Article = blogPost; // ✅ 完全可行

這種行為一開始可能會讓人意外。blogPost 物件有額外的屬性(categoryviews),這些屬性並不在 Article 介面中,但 TypeScript 仍然接受它。這是因為結構化型別系統問的是:「這個物件至少具備必要的屬性嗎?」如果是,就相容。

這種彈性其實很強大。我們可以讓同一個物件搭配多個介面使用,每個介面專注於它關心的屬性。

interface Publishable {
  publishedAt: string;
}

interface Viewable {
  views: number;
}

// 同一個 blogPost 物件可以搭配三個介面使用
const canPublish: Publishable = blogPost; // ✅ 有 publishedAt
const canTrack: Viewable = blogPost; // ✅ 有 views
const canRead: Article = blogPost; // ✅ 有 title、content、publishedAt

實務上的應用場景

介面解決了我們在開發應用程式時面臨的實際問題。

API 回應處理: 與其猜測 API 會回傳什麼結構,我們可以定義期望的確切結構:

interface ArticleResponse {
  data: {
    articles: Article[];
    totalCount: number;
  };
  status: "success" | "error";
  message?: string; // 錯誤情況下的可選屬性
}

const fetchArticles = async (): Promise<ArticleResponse> => {
  const response = await fetch("/api/articles");
  const data = await response.json();

  // TypeScript 現在知道 data 包含什麼內容
  // 如果 API 改變,我們會在編譯期間收到錯誤
  return data;
};

狀態管理: 在 React 應用程式中,介面幫助確保狀態更新維持正確的結構:

interface AppState {
  articles: Article[];
  selectedArticle: Article | null;
  isLoading: boolean;
  error: string | null;
}

const [state, setState] = useState<AppState>({
  articles: [],
  selectedArticle: null,
  isLoading: false,
  error: null,
});

// TypeScript 防止我們設定無效的狀態
setState({ articles: "not an array" });
// ❌ 錯誤:Type 'string' is not assignable to type 'Article[]'

Interface vs Type:應該選擇哪一個?

談論 interface 時,開發者不免會問一個實務問題:「我應該用 type 嗎?」這是合理的疑問,因為兩種語法都可以定義完全相同的物件結構,而且編輯器對它們的處理方式也相同。舉例來說,兩者都可以定義物件結構:

// 使用 interface
interface User {
  name: string;
  email: string;
}

// 使用 type 別名
type User = {
  name: string;
  email: string;
};

type 是我們的預設選擇

但我們還是需要做選擇,而且在理解每個選項真正的設計目的之前,這個選擇感覺很隨意。讓我們來了解什麼時候差異真正重要。

關鍵在於 type 幾乎可以做到 interface 能做的所有事情,而且還有一些 interface 做不到的。這就是為什麼許多開發者預設選擇 type,因為它是更靈活的選擇。

以下是 type 可以做但 interface 做不到的事情:

// 聯集型別 - 結合多種可能性
type Status = "draft" | "published" | "archived";

// 函式型別
type EventHandler = (event: Event) => void;

// 基於其他型別的計算型別
type ArticleKeys = keyof Article; // 得到 'title' | 'content' | 'publishedAt'

嘗試用 interface 寫這些會得到錯誤。我們無法用介面語法表達「這些值中的一個」或「具有特定簽名的函式」。

那什麼時候會選擇 interface

首先,當我們在建立類似類別的階層與繼承時。 有時候我們有多個型別共享共同屬性。例如,文章、留言和使用者可能都有 idcreatedAt 時間戳記。與其在每個介面中重複這些屬性,我們可以建立基礎介面並擴展它。

我們可以用 type 搭配交集型別(&)達到相同效果,但當我們在建立這些階層時,interface extends 語法更具可讀性。此外,interface 中的 extends 讓 TypeScript 的型別檢查器執行得比在 type 中使用 & 稍微快一些。

interface BaseEntity {
  id: string;
  createdAt: string;
}

// Article 自動擁有 id 和 createdAt
interface Article extends BaseEntity {
  title: string;
  content: string;
}

// Comment 也自動擁有 id 和 createdAt
interface Comment extends BaseEntity {
  text: string;
  authorId: string;
}

其次,當我們需要宣告合併時。 宣告合併是 TypeScript 的一個功能,多個同名的介面會自動合併成一個。這一開始聽起來很奇怪,但它解決了一個實際問題。

這什麼時候有用?主要是在使用第三方函式庫時。例如,如果我們使用的函式庫定義了 Window 介面,但我們需要在全域 window 物件中加入自己的屬性:

// 函式庫定義:
interface Window {
  location: Location;
  document: Document;
}

// 我們可以透過宣告相同介面來擴展它:
interface Window {
  myCustomProperty: string;
}

// 現在在 TypeScript 眼中,Window 會像這樣
interface Window {
  location: Location;
  document: Document;
  myCustomProperty: string;
}

缺點是宣告合併可能會意外發生。如果我們本來想建立兩個不同的型別但使用了相同名稱,TypeScript 會無聲地合併它們,而不是給我們錯誤。使用 type 時,如果意外宣告了相同名稱兩次,會得到清楚的錯誤,這能防止混淆。

讓選擇保持一致

了解關鍵差異後,最重要的決定是建立一致性。在類似用途上混合使用 interface 和 type 會為所有處理程式碼的人帶來不必要的困擾。

當開發者遇到不一致的模式時,自然會疑惑:「為什麼這裡用 interface 但那裡用 type?這是故意的還是意外?」這種疑問在混合方法的程式碼庫中會不斷出現,分散了對實際功能開發的注意力。

因此,設定一個規則,然後一致地遵循它。任何一種選擇都可以。一致性帶來的心理清晰度才是真正重要的。

總結

TypeScript 介面解決了 JavaScript 開發中的一個根本問題:物件可以有任何形狀,而且我們無法確知假設是否正確,直到執行期間。透過定義介面,我們建立了編譯期間的約定,在形狀不匹配的問題到達使用者之前就捕捉到它們。

介面中的兩個基本概念是:

  • 形狀約定: 介面保證物件具有我們期望的屬性,在編譯期間而非執行期間捕捉錯誤。

  • 結構化型別: TypeScript 使用結構化型別,所以任何具有正確屬性的物件都符合我們的介面,無論它是如何建立的。

關於 interface vs type 的選擇,在大多數物件結構上預設使用 type,因為它更靈活。只有在需要物件繼承使用 extends 或故意的宣告合併時才使用 interface

現在我們理解了如何為物件定義自訂型別,準備好探索 TypeScript 類別與物件,了解 TypeScript 如何透過靜態型別和存取修飾符來增強 JavaScript 基於類別的物件導向程式設計。


加入 E+ 成長計畫

如果你覺得這篇內容有幫助,喜歡我們的內容,歡迎加入 ExplainThis 籌辦的 E+ 成長計畫,透過每週的深度主題文,以及技術討論社群,讓讀者在前端、後端、軟體工程的領域上持續成長。

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們