Skip to content

映射类型

映射类型(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 }
// 将所有属性包装为 Promise
type 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 和 setter
type 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转换属性类型

下一篇我们将学习条件类型,了解如何根据条件选择类型。