映射类型(Mapped Types)允许我们基于现有类型创建新类型,通过遍历键来转换每个属性。
基础映射#
使用 in 关键字遍历类型的键:
// TypeScript 5.x
// 基本语法type Mapped<T> = { [K in keyof T]: T[K]}
interface User { id: number name: string email: string}
// 创建相同类型type UserCopy = Mapped<User>// { id: number; name: string; email: string }
// 将所有属性变为可选type MyPartial<T> = { [K in keyof T]?: T[K]}
type PartialUser = MyPartial<User>// { id?: number; name?: string; email?: string }
// 将所有属性变为只读type MyReadonly<T> = { readonly [K in keyof T]: T[K]}
type ReadonlyUser = MyReadonly<User>// { readonly id: number; readonly name: string; readonly email: string }修饰符控制#
使用 + 和 - 添加或移除修饰符:
interface OptionalUser { id?: number name?: string email?: string}
// 移除可选修饰符type Required<T> = { [K in keyof T]-?: T[K]}
type RequiredUser = Required<OptionalUser>// { id: number; name: string; email: string }
// 移除只读修饰符interface ReadonlyUser { readonly id: number readonly name: string}
type Mutable<T> = { -readonly [K in keyof T]: T[K]}
type MutableUser = Mutable<ReadonlyUser>// { id: number; name: string }
// 同时控制多个修饰符type FullyMutable<T> = { -readonly [K in keyof T]-?: T[K]}
interface StrictUser { readonly id?: number readonly name?: string}
type FlexibleUser = FullyMutable<StrictUser>// { id: number; name: string }值类型转换#
映射时转换属性的值类型:
// 将所有属性变为字符串type Stringify<T> = { [K in keyof T]: string}
interface Product { id: number name: string price: number inStock: boolean}
type StringProduct = Stringify<Product>// { id: string; name: string; price: string; inStock: string }
// 将所有属性包装为 Promisetype Promisify<T> = { [K in keyof T]: Promise<T[K]>}
type AsyncProduct = Promisify<Product>// { id: Promise<number>; name: Promise<string>; ... }
// 将所有属性变为数组type Arrayify<T> = { [K in keyof T]: T[K][]}
type ArrayProduct = Arrayify<Product>// { id: number[]; name: string[]; ... }
// 条件转换type Nullable<T> = { [K in keyof T]: T[K] | null}
type NullableProduct = Nullable<Product>// { id: number | null; name: string | null; ... }键重映射#
使用 as 子句重命名键:
// 添加前缀type Prefixed<T, P extends string> = { [K in keyof T as `${P}${Capitalize<K & string>}`]: T[K]}
interface User { name: string age: number}
type GetterUser = Prefixed<User, 'get'>// { getName: string; getAge: number }
// 添加后缀type Suffixed<T, S extends string> = { [K in keyof T as `${K & string}${S}`]: T[K]}
type UserHandler = Suffixed<User, 'Handler'>// { nameHandler: string; ageHandler: 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 OmitByType<T, U> = { [K in keyof T as T[K] extends U ? never : K]: T[K]}
type WithoutNumbers = OmitByType<Mixed, number>// { name: string; email: string; active: boolean }Getter/Setter 生成#
// 生成 getter 类型type Getters<T> = { [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K]}
// 生成 setter 类型type Setters<T> = { [K in keyof T as `set${Capitalize<K & string>}`]: (value: T[K]) => void}
interface State { count: number name: string loading: boolean}
type StateGetters = Getters<State>// {// getCount: () => number// getName: () => string// getLoading: () => boolean// }
type StateSetters = Setters<State>// {// setCount: (value: number) => void// setName: (value: string) => void// setLoading: (value: boolean) => void// }
// 组合 getter 和 settertype WithAccessors<T> = T & Getters<T> & Setters<T>
type FullState = WithAccessors<State>深度映射#
递归处理嵌套对象:
// 深度只读type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? T[K] extends Function ? T[K] : DeepReadonly<T[K]> : T[K]}
interface NestedConfig { database: { host: string port: number credentials: { username: string password: string } } server: { port: number }}
type ReadonlyConfig = DeepReadonly<NestedConfig>// 所有嵌套属性都变为只读
// 深度可选type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? T[K] extends Function ? T[K] : DeepPartial<T[K]> : T[K]}
type PartialConfig = DeepPartial<NestedConfig>// 所有嵌套属性都变为可选
// 深度必需type DeepRequired<T> = { [K in keyof T]-?: T[K] extends object ? T[K] extends Function ? T[K] : DeepRequired<T[K]> : T[K]}条件映射#
根据属性类型应用不同转换:
// 根据类型条件转换type ConditionalMap<T> = { [K in keyof T]: T[K] extends string ? `string:${T[K]}` : T[K] extends number ? `number:${T[K]}` : T[K]}
// 提取特定类型的键type KeysOfType<T, U> = { [K in keyof T]: T[K] extends U ? K : never}[keyof T]
interface User { id: number name: string email: string age: number active: boolean}
type StringKeys = KeysOfType<User, string>// 'name' | 'email'
type NumberKeys = KeysOfType<User, number>// 'id' | 'age'
// 只保留特定类型的属性type PickByType<T, U> = { [K in keyof T as T[K] extends U ? K : never]: T[K]}
type StringProps = PickByType<User, string>// { name: string; email: string }实际应用#
表单验证类型#
interface FormFields { username: string email: string age: number agreed: boolean}
// 生成错误类型type FormErrors<T> = { [K in keyof T]?: string}
// 生成触摸状态类型type FormTouched<T> = { [K in keyof T]?: boolean}
// 生成验证器类型type FormValidators<T> = { [K in keyof T]?: (value: T[K]) => string | undefined}
// 完整表单状态interface FormState<T> { values: T errors: FormErrors<T> touched: FormTouched<T>}
const formState: FormState<FormFields> = { values: { username: '', email: '', age: 0, agreed: false, }, errors: { username: '用户名不能为空', }, touched: { username: true, email: true, },}API 响应转换#
interface ApiEntity { id: number created_at: string updated_at: string}
interface User extends ApiEntity { name: string email: string}
// 驼峰转换类型type CamelCase<S extends string> = S extends `${infer P}_${infer Q}` ? `${P}${Capitalize<CamelCase<Q>>}` : S
type CamelCaseKeys<T> = { [K in keyof T as CamelCase<K & string>]: T[K]}
type CamelUser = CamelCaseKeys<User>// { id: number; createdAt: string; updatedAt: string; name: string; email: string }
// 添加时间戳类型转换type WithDates<T> = { [K in keyof T]: K extends 'created_at' | 'updated_at' ? Date : K extends `${string}_at` ? Date : T[K]}
type UserWithDates = WithDates<User>// { id: number; created_at: Date; updated_at: Date; name: string; email: string }事件处理器映射#
interface Events { click: { x: number; y: number } focus: { target: HTMLElement } input: { value: string }}
// 生成事件处理器类型type EventHandlers<T> = { [K in keyof T as `on${Capitalize<K & string>}`]: (event: T[K]) => void}
type AppEventHandlers = EventHandlers<Events>// {// onClick: (event: { x: number; y: number }) => void// onFocus: (event: { target: HTMLElement }) => void// onInput: (event: { value: string }) => void// }
// 生成事件监听器方法type EventListenerMethods<T> = { [K in keyof T as `add${Capitalize<K & string>}Listener`]: ( handler: (event: T[K]) => void ) => void} & { [K in keyof T as `remove${Capitalize<K & string>}Listener`]: ( handler: (event: T[K]) => void ) => void}
type EventEmitter = EventListenerMethods<Events>状态管理#
interface State { user: { name: string } | null count: number loading: boolean}
// 生成 action 类型type Actions<T> = { [K in keyof T as `set${Capitalize<K & string>}`]: { type: `SET_${Uppercase<K & string>}` payload: T[K] }}[keyof T]
type AppAction = Actions<State>// { type: 'SET_USER'; payload: { name: string } | null }// | { type: 'SET_COUNT'; payload: number }// | { type: 'SET_LOADING'; payload: boolean }
// 生成 reducer 类型type Reducers<T> = { [K in keyof T]: (state: T, payload: T[K]) => T}常见问题#
🙋 映射类型会保留可选性吗?#
interface Original { required: string optional?: number}
// 默认保留修饰符type Mapped = { [K in keyof Original]: Original[K]}// { required: string; optional?: number }
// 显式保留type PreserveModifiers<T> = { [K in keyof T]: T[K]}
// 移除可选type MakeRequired<T> = { [K in keyof T]-?: T[K]}// { required: string; optional: number }🙋 如何只映射部分键?#
interface User { id: number name: string email: string password: string}
// 使用 Pick 限制键type PublicUser = { [K in keyof Pick<User, 'id' | 'name' | 'email'>]: User[K]}
// 或者使用 as 过滤type ExcludePassword = { [K in keyof User as Exclude<K, 'password'>]: User[K]}🙋 映射类型可以添加新属性吗?#
// 映射类型只能转换现有属性// 使用交叉类型添加新属性type WithTimestamp<T> = { [K in keyof T]: T[K]} & { timestamp: Date}
interface User { name: string}
type TimestampedUser = WithTimestamp<User>// { name: string } & { timestamp: Date }总结#
| 特性 | 语法 | 用途 |
|---|---|---|
| 基础映射 | [K in keyof T]: T[K] | 遍历所有属性 |
| 添加修饰符 | +readonly, +? | 添加只读/可选 |
| 移除修饰符 | -readonly, -? | 移除只读/可选 |
| 键重映射 | as NewKey | 重命名或过滤键 |
| 值转换 | : NewType | 转换属性类型 |
下一篇我们将学习条件类型,了解如何根据条件选择类型。