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同時具備User與Timestamped的所有屬性,避免手動複製欄位。
範例 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。 |
| 過度抽象 | 把每個小型物件都寫成別名,會讓程式碼層級過深,閱讀時需要不斷跳躍。 | 只為 有意義的業務概念 建立別名,避免對純粹的臨時結構使用別名。 |
| 別名與介面的混用 | 有時同一個概念同時出現在 type 與 interface 定義,造成型別不一致。 |
統一風格:若需要 擴充(declaration merging),使用 interface;若要 聯合、交叉、條件,使用 type。 |
| 忘記匯出別名 | 在多檔案專案中,未將別名 export,導致其他模組無法使用。 |
建議將所有公共別名放在 types/*.d.ts 或 src/models/*.ts 中,統一匯出。 |
過度使用 any |
為了避免寫太多別名,直接使用 any,失去型別安全。 |
利用 unknown + 型別守護(type guard),或先寫最小化的別名,再逐步完善。 |
最佳實踐
- 命名規則:別名名稱通常使用 PascalCase(如
UserId,ApiResult),與介面保持一致,讓讀者一眼辨識是型別。 - 分層管理:將通用型別(如
Id,Paginated)放在src/types/common.ts,業務模型放在src/types/models.ts,保持專案結構清晰。 - 文件化:為每個別名加上 JSDoc 註解,IDE 會自動顯示說明,提升團隊協作效率。
- 避免過度嵌套:如果別名的結構已超過三層深度,考慮拆分成多個別名或改用介面。
- 使用
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 常需定義 Action、State、Payload。別名可以讓每個 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 客戶端 等多個層面統一型別定義。
- 結合
readonly、Partial、Pick、Record等內建型別,快速構建符合業務需求的型別模型。
在實務開發中,適度使用型別別名能讓團隊 快速定位錯誤、減少重複程式碼、提升開發效率。然而,別名也不是萬能的;過度抽象、循環引用或混用 type 與 interface 都可能帶來維護負擔。遵守命名規則、分層管理、文件化說明,以及在需要擴充的情況下使用介面,是保持型別別名健康的最佳策略。
最後的建議:在新專案或模組化開發時,先規劃好公共型別別名的檔案結構,並配合 CI 檢查(如
tsc --noEmit)確保型別一致性。隨著專案成長,型別別名將成為你最值得信賴的「型別資產」——它不僅是編譯器的輔助工具,更是團隊協作與程式品質的基石。祝你在 TypeScript 的世界裡寫出更乾淨、更安全的程式碼! 🚀