假設我們有一個型別:
ts
CopyEdit
type User = {
id: string;
name: string;
email: string;
};
如果今天要讓所有屬性變成 可選(optional),我們可能會:
ts
CopyEdit
type PartialUser = {
id?: string;
name?: string;
email?: string;
};
但這樣一個一個改超累,而且很不 DRY(Don't Repeat Yourself)。
映射型別(Mapped Types) 就是為了「批量改造型別」而生的,
它可以讓我們用迴圈的概念一次處理所有屬性。
ts
CopyEdit
type MyMapped<T> = {
[K in keyof T]: T[K];
};
這裡:
keyof T
:取得型別 T 的所有鍵名(union 型別)K in keyof T
:遍歷每一個鍵T[K]
:取出該鍵的型別範例:
ts
CopyEdit
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
TS 內建了幾個常用映射型別:
Partial<T>
:所有屬性變成可選Required<T>
:所有屬性變成必填Readonly<T>
:所有屬性變成唯讀Pick<T, K>
:挑選部分屬性Omit<T, K>
:刪除部分屬性Record<K, T>
:建立一個以 K 為鍵、T 為值的型別範例:
ts
CopyEdit
type PartialUser = Partial<User>;
type ReadonlyUser = Readonly<User>;
type IdNameOnly = Pick<User, "id" | "name">;
我們可以在映射型別裡用條件型別(Day 24 學過的技巧):
ts
CopyEdit
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
也可以根據屬性型別做判斷:
ts
CopyEdit
type NullableOnlyString<T> = {
[K in keyof T]: T[K] extends string ? string | null : T[K];
};
TS 4.1 之後,映射型別支援 as
關鍵字來重新命名鍵名:
ts
CopyEdit
type RenameKeys<T, M extends Record<string, string>> = {
[K in keyof T as K extends keyof M ? M[K] : K]: T[K];
};
type UserRenamed = RenameKeys<User, { id: "userId"; name: "fullName" }>;
// { userId: string; fullName: string; email: string }
後端回傳:
ts
CopyEdit
type UserApiResponse = {
id: string;
name: string;
email: string;
created_at: string;
};
前端想要駝峰式:
ts
CopyEdit
type SnakeToCamel<T> = {
[K in keyof T as K extends `${infer First}_${infer Rest}`
? `${First}${Capitalize<Rest>}`
: K]: T[K];
};
type UserCamel = SnakeToCamel<UserApiResponse>;
假設我們想從資料型別生成表單設定:
ts
CopyEdit
type FieldConfig<T> = {
[K in keyof T]: {
label: string;
value: T[K];
required: boolean;
};
};
type UserForm = FieldConfig<User>;
這樣每個欄位的 value 型別會對應資料型別,非常安全。
ts
CopyEdit
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type UserWithOptionalEmail = Optional<User, "email">;
錯誤 1:忘了使用 keyof T
ts
CopyEdit
type Wrong<T> = {
[K in T]: T[K]; // ❌ T 不是物件鍵
};
錯誤 2:忽略 as
可以重新命名鍵
錯誤 3:過度映射
Pick
/ Omit
搭配手動定義會更簡潔