TypeScript 映射型別 (Mapped Types)
映射型別 (Mapped Types) 允許我們基於現有型別建立新型別,透過迭代物件型別的鍵來轉換每個屬性。這是 TypeScript 型別系統中非常強大的功能。
基本語法
type MappedType<T> = {
[K in keyof T]: NewType;
};
keyof T:取得 T 的所有鍵K in keyof T:迭代所有鍵NewType:新的屬性型別
基本範例
將所有屬性變成可選
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = MyPartial<User>;
// { id?: number; name?: string; email?: string }
將所有屬性變成唯讀
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
interface Point {
x: number;
y: number;
}
type ReadonlyPoint = MyReadonly<Point>;
// { readonly x: number; readonly y: number }
將所有屬性變成特定型別
type Stringify<T> = {
[K in keyof T]: string;
};
interface Config {
port: number;
host: string;
ssl: boolean;
}
type StringConfig = Stringify<Config>;
// { port: string; host: string; ssl: string }
屬性修飾子
新增修飾子
// 新增 readonly
type AddReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// 新增 optional
type AddOptional<T> = {
[K in keyof T]?: T[K];
};
移除修飾子
使用 - 移除修飾子:
// 移除 readonly
type RemoveReadonly<T> = {
-readonly [K in keyof T]: T[K];
};
// 移除 optional
type RemoveOptional<T> = {
[K in keyof T]-?: T[K];
};
// TypeScript 內建的 Required<T> 就是這樣實作的
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
鍵的重新映射 (Key Remapping)
TypeScript 4.1+ 支援使用 as 重新映射鍵:
// 將所有鍵改為大寫
type UppercaseKeys<T> = {
[K in keyof T as Uppercase<K & string>]: T[K];
};
interface User {
name: string;
age: number;
}
type UpperUser = UppercaseKeys<User>;
// { NAME: string; AGE: number }
過濾鍵
// 只保留字串型別的屬性
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
email: string;
active: boolean;
}
type StringProps = OnlyStrings<Mixed>;
// { name: string; email: string }
為鍵加上前綴
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
type Setters<T> = {
[K in keyof T as `set${Capitalize<K & string>}`]: (value: T[K]) => void;
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
type PersonSetters = Setters<Person>;
// { setName: (value: string) => void; setAge: (value: number) => void }
條件映射
// 將函式型別包裝成 Promise
type Promisify<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R ? (...args: A) => Promise<R> : T[K];
};
interface SyncApi {
getUser(id: number): User;
saveUser(user: User): void;
version: string;
}
type AsyncApi = Promisify<SyncApi>;
// {
// getUser(id: number): Promise<User>;
// saveUser(user: User): Promise<void>;
// version: string;
// }
實用映射型別
Nullable<T>
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
interface User {
name: string;
email: string;
}
type NullableUser = Nullable<User>;
// { name: string | null; email: string | null }
Mutable<T>
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
interface ReadonlyUser {
readonly id: number;
readonly name: string;
}
type MutableUser = Mutable<ReadonlyUser>;
// { id: number; name: string }
DeepPartial<T>
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
interface NestedConfig {
server: {
host: string;
port: number;
};
logging: {
level: string;
format: string;
};
}
type PartialConfig = DeepPartial<NestedConfig>;
// 所有巢狀屬性都變成可選
DeepReadonly<T>
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
映射型別與聯合型別
type EventHandlers<Events extends string> = {
[K in Events as `on${Capitalize<K>}`]: (event: K) => void;
};
type MouseEvents = 'click' | 'mouseenter' | 'mouseleave';
type MouseEventHandlers = EventHandlers<MouseEvents>;
// {
// onClick: (event: "click") => void;
// onMouseenter: (event: "mouseenter") => void;
// onMouseleave: (event: "mouseleave") => void;
// }
進階範例
物件方法轉換
type MethodsOnly<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
type PropertiesOnly<T> = {
[K in keyof T as T[K] extends Function ? never : K]: T[K];
};
class User {
name: string = '';
age: number = 0;
greet() {
return `Hello, ${this.name}`;
}
getAge() {
return this.age;
}
}
type UserMethods = MethodsOnly<User>;
// { greet: () => string; getAge: () => number }
type UserProperties = PropertiesOnly<User>;
// { name: string; age: number }
建立 Form 型別
type FormFields<T> = {
[K in keyof T]: {
value: T[K];
error: string | null;
touched: boolean;
};
};
interface LoginData {
email: string;
password: string;
}
type LoginForm = FormFields<LoginData>;
// {
// email: { value: string; error: string | null; touched: boolean };
// password: { value: string; error: string | null; touched: boolean };
// }
API 端點型別
type ApiEndpoints<T> = {
[K in keyof T as `fetch${Capitalize<K & string>}`]: () => Promise<T[K]>;
} & {
[K in keyof T as `update${Capitalize<K & string>}`]: (data: Partial<T[K]>) => Promise<T[K]>;
} & {
[K in keyof T as `delete${Capitalize<K & string>}`]: (id: number) => Promise<void>;
};
interface Resources {
user: User;
post: Post;
}
type ResourceApi = ApiEndpoints<Resources>;
// {
// fetchUser: () => Promise<User>;
// updateUser: (data: Partial<User>) => Promise<User>;
// deleteUser: (id: number) => Promise<void>;
// fetchPost: () => Promise<Post>;
// updatePost: (data: Partial<Post>) => Promise<Post>;
// deletePost: (id: number) => Promise<void>;
// }
狀態與動作
type Actions<State> = {
[K in keyof State as `set${Capitalize<K & string>}`]: (value: State[K]) => void;
} & {
[K in keyof State as `reset${Capitalize<K & string>}`]: () => void;
};
interface AppState {
count: number;
user: User | null;
theme: 'light' | 'dark';
}
type AppActions = Actions<AppState>;
// {
// setCount: (value: number) => void;
// resetCount: () => void;
// setUser: (value: User | null) => void;
// resetUser: () => void;
// setTheme: (value: "light" | "dark") => void;
// resetTheme: () => void;
// }
保留原始型別資訊
// 保留可選和唯讀修飾子
type Clone<T> = {
[K in keyof T]: T[K];
};
// 這會保留所有原始的修飾子
interface Original {
readonly id: number;
name?: string;
email: string;
}
type Cloned = Clone<Original>;
// { readonly id: number; name?: string; email: string }
總結
映射型別是 TypeScript 中最強大的型別操作工具之一,它可以:
- 基於現有型別建立新型別
- 新增或移除屬性修飾子
- 過濾或重新命名鍵
- 轉換屬性型別
- 建立複雜的型別轉換