本文 AI 產出,尚未審核

TypeScript 基本型別 ── 型別別名(Type Alias)


簡介

在日常開發中,我們常會遇到需要重複使用相同結構的型別,例如一個使用者物件、API 回傳的資料格式或是函式的參數型別。若每次都直接寫出完整的型別描述,程式碼不僅冗長,也不易維護。**型別別名(Type Alias)**正是為了解決這類問題而設計的,它讓我們可以為任意型別(基本型別、聯合型別、交叉型別、物件型別…)取一個易讀的名字。

使用型別別名可以:

  • 提升可讀性:一次看到別名就能了解資料的意圖,而不必逐行解讀長長的型別描述。
  • 減少重複:同一型別在多個檔案或函式中出現時,只要修改別名的定義,即可同步更新所有使用處。
  • 加強型別安全:透過別名,我們可以在編譯階段捕捉到不符合預期的資料結構,降低執行時錯誤的機會。

本篇將從概念說明、實作範例、常見陷阱與最佳實踐,一直到真實專案中的應用情境,帶你完整掌握 TypeScript 的型別別名。


核心概念

1. 基本語法

型別別名使用關鍵字 type,後接別名名稱、等號 =,以及要別名化的型別。

type UserId = number;
type Username = string;
type Point = { x: number; y: number };
  • 別名本身 不是 新的型別,它只是原型別的另一個名字。
  • 別名可以用在任何需要型別的地方:變數、函式參數、回傳值、泛型(generics)等。

2. 聯合型別(Union)與交叉型別(Intersection)別名

聯合型別表示「可以是 A 或 B」,交叉型別則表示「同時具備 A 與 B 的成員」。

type SuccessResponse = { status: "ok"; data: any };
type ErrorResponse   = { status: "error"; message: string };

type ApiResponse = SuccessResponse | ErrorResponse;   // 聯合型別別名
type UserWithMeta = User & { createdAt: Date };       // 交叉型別別名

透過別名,我們可以把複雜的型別組合抽象成易懂的詞彙。

3. 泛型型別別名

型別別名同樣支援泛型,讓我們在定義時保有彈性。

type Paginated<T> = {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
};

type UserPage = Paginated<User>;

4. 條件型別(Conditional Types)別名

條件型別是 TypeScript 2.8 引入的高階型別技巧,結合別名能寫出非常強大的型別工具。

type IsString<T> = T extends string ? true : false;

type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false

5. 內建型別與自訂型別的混用

我們常會把內建型別(如 Record, Partial, Pick)與自訂別名結合,產生更貼近業務需求的型別。

type User = {
  id: UserId;
  name: Username;
  email: string;
  role: "admin" | "member" | "guest";
};

type PartialUser = Partial<User>;               // 讓所有屬性變成可選
type UserSummary = Pick<User, "id" | "name">;   // 只挑出 id、name
type RoleMap = Record<User["role"], UserId[]>; // 以角色為鍵,對應使用者 ID 陣列

程式碼範例

以下提供 5 個實用範例,從最基礎到較進階的應用,說明型別別名在日常開發中的威力。

範例 1️⃣ 基本別名與函式結合

type UserId = number;

function getUserName(id: UserId): string {
  // 假設從資料庫取得使用者名稱
  return `User_${id}`;
}

// 呼叫時會自動檢查型別
const name = getUserName(42);   // 正確
// const wrong = getUserName("42"); // 編譯錯誤:Argument of type 'string' is not assignable to parameter of type 'UserId'.

重點:即使 UserId 只是 number 的別名,IDE 仍會顯示「UserId」而非「number」,提升可讀性。

範例 2️⃣ 聯合型別別名:API 回傳結果

type Success = {
  ok: true;
  payload: any;
};

type Failure = {
  ok: false;
  error: string;
};

type ApiResult = Success | Failure; // 別名化的聯合型別

function fetchData(): ApiResult {
  // 模擬隨機成功或失敗
  return Math.random() > 0.5
    ? { ok: true, payload: { data: "Hello" } }
    : { ok: false, error: "Network error" };
}

// 使用時可以透過 discriminated union 直接判斷
const result = fetchData();
if (result.ok) {
  console.log("Data:", result.payload);
} else {
  console.error("Error:", result.error);
}

技巧ok 屬性作為 discriminant,讓 TypeScript 能自動窄化型別。

範例 3️⃣ 交叉型別別名:擴充既有型別

type Timestamped = {
  createdAt: Date;
  updatedAt: Date;
};

type User = {
  id: UserId;
  name: Username;
};

type UserRecord = User & Timestamped; // 交叉型別別名

const admin: UserRecord = {
  id: 1,
  name: "admin",
  createdAt: new Date(),
  updatedAt: new Date(),
};

說明UserRecord 同時具備 UserTimestamped 的所有屬性,避免手動複製欄位。

範例 4️⃣ 泛型型別別名:分頁資料

type Paginated<T> = {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
};

type Product = {
  sku: string;
  price: number;
};

type ProductPage = Paginated<Product>;

function loadProducts(page: number, pageSize: number): ProductPage {
  // 假設從 API 取得分頁結果
  const items: Product[] = [
    { sku: "A001", price: 199 },
    { sku: "A002", price: 299 },
  ];
  return {
    items,
    total: 42,
    page,
    pageSize,
  };
}

好處Paginated<T> 只寫一次,就能為任何資料型別生成分頁結構。

範例 5️⃣ 條件型別與映射型別:從字面值產生型別

// 把字串字面值映射成對應的事件型別
type EventMap = {
  click: MouseEvent;
  keypress: KeyboardEvent;
  scroll: Event;
};

type EventHandler<K extends keyof EventMap> = (e: EventMap[K]) => void;

function addListener<K extends keyof EventMap>(type: K, handler: EventHandler<K>) {
  window.addEventListener(type, handler as EventListener);
}

// 正確使用
addListener("click", (e) => {
  console.log(e.clientX, e.clientY); // e 被推斷為 MouseEvent
});

// 錯誤示範(編譯期會報錯)
// addListener("click", (e) => console.log(e.key)); // Property 'key' does not exist on type 'MouseEvent'.

要點:利用 keyof 與條件型別,我們可以在 編譯期 完全保證事件處理函式的參數型別正確。


常見陷阱與最佳實踐

陷阱 說明 解決方案
別名循環(Recursive Alias) 定義 type A = B & {},而 B 又引用 A,會導致編譯錯誤或無限遞迴。 使用 介面(interface) 來描述自我參照的結構,或改成 交叉型別 + Partial
過度抽象 把每個小型物件都寫成別名,會讓程式碼層級過深,閱讀時需要不斷跳躍。 只為 有意義的業務概念 建立別名,避免對純粹的臨時結構使用別名。
別名與介面的混用 有時同一個概念同時出現在 typeinterface 定義,造成型別不一致。 統一風格:若需要 擴充(declaration merging),使用 interface;若要 聯合、交叉、條件,使用 type
忘記匯出別名 在多檔案專案中,未將別名 export,導致其他模組無法使用。 建議將所有公共別名放在 types/*.d.tssrc/models/*.ts 中,統一匯出。
過度使用 any 為了避免寫太多別名,直接使用 any,失去型別安全。 利用 unknown + 型別守護(type guard),或先寫最小化的別名,再逐步完善。

最佳實踐

  1. 命名規則:別名名稱通常使用 PascalCase(如 UserId, ApiResult),與介面保持一致,讓讀者一眼辨識是型別。
  2. 分層管理:將通用型別(如 Id, Paginated)放在 src/types/common.ts,業務模型放在 src/types/models.ts,保持專案結構清晰。
  3. 文件化:為每個別名加上 JSDoc 註解,IDE 會自動顯示說明,提升團隊協作效率。
  4. 避免過度嵌套:如果別名的結構已超過三層深度,考慮拆分成多個別名或改用介面。
  5. 使用 readonly:在別名描述不可變資料時,加上 readonly,讓 TypeScript 在編譯期阻止不小心的變更。
/** 使用者唯一識別碼 */
export type UserId = number;

/** 只讀的點座標 */
export type Point = readonly [number, number];

實際應用場景

1. 前端 UI 元件庫的 Props 定義

在 React、Vue、Angular 等框架中,元件的 props 常常需要複雜的型別描述。使用別名可以把共用的屬性抽離,減少重複。

// sharedTypes.ts
export type Size = "xs" | "sm" | "md" | "lg" | "xl";
export type Color = "primary" | "secondary" | "danger" | "success";

// Button.tsx
import { Size, Color } from "./sharedTypes";

type ButtonProps = {
  label: string;
  size?: Size;
  color?: Color;
  disabled?: boolean;
};

2. 後端 API 客戶端的回傳型別

在與 REST 或 GraphQL API 互動時,常會有 成功/失敗 兩種回傳結構。用別名把它們包成一個 ApiResponse<T>,可以在服務層一次解決型別推斷。

type ApiSuccess<T> = { ok: true; data: T };
type ApiError = { ok: false; error: string };
type ApiResponse<T> = ApiSuccess<T> | ApiError;

// 使用範例
async function getUser(id: UserId): Promise<ApiResponse<User>> {
  const res = await fetch(`/api/users/${id}`);
  if (res.ok) return { ok: true, data: await res.json() };
  return { ok: false, error: res.statusText };
}

3. 狀態管理(Redux / Zustand / Pinia)

大型專案的 Redux store 常需定義 ActionStatePayload。別名可以讓每個 slice 的型別保持一致,又不失彈性。

type Action<T = any> = {
  type: string;
  payload?: T;
};

type CounterState = {
  count: number;
};

type Increment = Action<null>;
type Add = Action<number>;

type CounterAction = Increment | Add;

4. 多語系(i18n)字串映射

將語系檔案的鍵值對抽成別名,能在編寫 UI 時即時得到鍵名的自動完成。

type LocaleKey = keyof typeof import("./locales/en.json");
type Translations = Record<LocaleKey, string>;

function t(key: LocaleKey): string {
  // 假設已載入對應語系檔
  return (translations as Translations)[key];
}

// 使用
const hello = t("welcome_message"); // IDE 會提示可用的 key

5. 測試資料的生成(Factory)

在 unit test 中,我們會使用 factory function 產生測試物件。別名讓測試資料的型別與實際模型保持同步。

type UserFactory = (overrides?: Partial<User>) => User;

const createUser: UserFactory = (overrides = {}) => ({
  id: 0,
  name: "TestUser",
  email: "test@example.com",
  role: "member",
  ...overrides,
});

總結

型別別名是 TypeScript 中提升程式碼可讀性、可維護性與型別安全的關鍵工具。透過 type 關鍵字,我們可以:

  • 為基本型別、物件型別、聯合與交叉型別、泛型、條件型別等建立 語意化的名稱
  • 函式參數、回傳值、資料結構、狀態管理、API 客戶端 等多個層面統一型別定義。
  • 結合 readonlyPartialPickRecord 等內建型別,快速構建符合業務需求的型別模型。

在實務開發中,適度使用型別別名能讓團隊 快速定位錯誤、減少重複程式碼、提升開發效率。然而,別名也不是萬能的;過度抽象、循環引用或混用 typeinterface 都可能帶來維護負擔。遵守命名規則、分層管理、文件化說明,以及在需要擴充的情況下使用介面,是保持型別別名健康的最佳策略。

最後的建議:在新專案或模組化開發時,先規劃好公共型別別名的檔案結構,並配合 CI 檢查(如 tsc --noEmit)確保型別一致性。隨著專案成長,型別別名將成為你最值得信賴的「型別資產」——它不僅是編譯器的輔助工具,更是團隊協作與程式品質的基石。祝你在 TypeScript 的世界裡寫出更乾淨、更安全的程式碼! 🚀