映射类型
概述
映射类型(Mapped Types)是 TypeScript 2.1 引入的强大特性,它允许我们基于现有类型创建新类型。映射类型通过遍历现有类型的属性,并根据某种规则转换这些属性,从而生成新的类型。映射类型使用 in keyof 语法来遍历类型的所有键,类似于 JavaScript 中的 for...in 循环,但作用于类型层面。通过映射类型,我们可以创建灵活且可复用的类型转换工具,实现类型级别的数据转换和操作。
为什么需要映射类型
在没有映射类型的情况下,我们需要手动创建类似的类型:
typescript
// 没有映射类型:需要手动创建每个变体
interface User {
name: string;
age: number;
email: string;
}
// 手动创建所有属性为可选的版本
interface PartialUser {
name?: string;
age?: number;
email?: string;
}
// 手动创建所有属性为只读的版本
interface ReadonlyUser {
readonly name: string;
readonly age: number;
readonly email: string;
}
// 使用映射类型:自动生成所有变体
type PartialUser = {
[K in keyof User]?: User[K];
};
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};映射类型让我们能够:
- 自动生成类型变体:基于现有类型自动创建新的类型
- 批量修改属性:一次性修改类型的所有属性
- 创建类型工具:构建可复用的类型转换工具
- 类型级编程:在类型层面实现数据转换逻辑
基本语法
基本映射类型
映射类型使用 [K in keyof T] 语法来遍历类型的所有键:
typescript
// 基本语法:[K in keyof T]
// K 是类型变量,代表 T 的每个键
// keyof T 获取 T 的所有键的联合类型
interface User {
name: string;
age: number;
email: string;
}
// 基本映射类型:复制所有属性
type CopyUser = {
[K in keyof User]: User[K];
};
// 等价于:
// type CopyUser = {
// name: string;
// age: number;
// email: string;
// };映射类型的工作原理
映射类型通过遍历类型的所有键,为每个键生成新的属性:
typescript
interface Config {
host: string;
port: number;
ssl: boolean;
}
// 映射类型遍历 Config 的每个键(host、port、ssl)
// 为每个键创建新属性,类型为 Config[K]
type ConfigCopy = {
[K in keyof Config]: Config[K];
};
// 结果类型:
// {
// host: string;
// port: number;
// ssl: boolean;
// }使用类型别名
映射类型通常与类型别名结合使用:
typescript
// 定义映射类型工具
type Optional<T> = {
[K in keyof T]?: T[K];
};
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
// 使用映射类型工具
interface User {
name: string;
age: number;
}
type OptionalUser = Optional<User>;
// 类型:{ name?: string; age?: number; }
type ReadonlyUser = Readonly<User>;
// 类型:{ readonly name: string; readonly age: number; }修饰符操作
映射类型支持添加和移除属性修饰符(readonly 和 ?):
添加修饰符
typescript
// 添加可选修饰符
type Partial<T> = {
[K in keyof T]?: T[K];
};
// 添加只读修饰符
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
// 同时添加两个修饰符
type PartialReadonly<T> = {
readonly [K in keyof T]?: T[K];
};
// 使用示例
interface User {
name: string;
age: number;
}
type PartialUser = Partial<User>;
// 类型:{ name?: string; age?: number; }
type ReadonlyUser = Readonly<User>;
// 类型:{ readonly name: string; readonly age: number; }
type PartialReadonlyUser = PartialReadonly<User>;
// 类型:{ readonly name?: string; readonly age?: number; }移除修饰符
使用 - 前缀可以移除修饰符:
typescript
// 移除可选修饰符(Required)
type Required<T> = {
[K in keyof T]-?: T[K];
};
// 移除只读修饰符
type Writable<T> = {
-readonly [K in keyof T]: T[K];
};
// 移除所有修饰符
type Mutable<T> = {
-readonly [K in keyof T]-?: T[K];
};
// 使用示例
interface User {
readonly name?: string;
readonly age?: number;
}
type RequiredUser = Required<User>;
// 类型:{ readonly name: string; readonly age: number; }
type WritableUser = Writable<User>;
// 类型:{ name?: string; age?: number; }
type MutableUser = Mutable<User>;
// 类型:{ name: string; age: number; }修饰符组合
可以同时操作多个修饰符:
typescript
// 移除只读,添加可选
type WritablePartial<T> = {
-readonly [K in keyof T]?: T[K];
};
// 移除可选,添加只读
type ReadonlyRequired<T> = {
readonly [K in keyof T]-?: T[K];
};
// 使用示例
interface User {
readonly name?: string;
readonly age?: number;
}
type WritablePartialUser = WritablePartial<User>;
// 类型:{ name?: string; age?: number; }
type ReadonlyRequiredUser = ReadonlyRequired<User>;
// 类型:{ readonly name: string; readonly age: number; }键的过滤和转换
映射类型支持通过条件类型过滤和转换键:
过滤特定键
typescript
// 只保留字符串类型的键
type StringKeys<T> = {
[K in keyof T as K extends string ? K : never]: T[K];
};
// 排除特定键
type Omit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
// 只选择特定键
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// 使用示例
interface User {
name: string;
age: number;
email: string;
id: number;
}
type UserWithoutId = Omit<User, 'id'>;
// 类型:{ name: string; age: number; email: string; }
type UserNameAndEmail = Pick<User, 'name' | 'email'>;
// 类型:{ name: string; email: string; }转换键名
typescript
// 为所有键添加前缀
type PrefixedKeys<T, Prefix extends string> = {
[K in keyof T as `${Prefix}${string & K}`]: T[K];
};
// 为所有键添加后缀
type SuffixedKeys<T, Suffix extends string> = {
[K in keyof T as `${string & K}${Suffix}`]: T[K];
};
// 将键转换为大写
type UppercaseKeys<T> = {
[K in keyof T as Uppercase<string & K>]: T[K];
};
// 使用示例
interface User {
name: string;
age: number;
}
type PrefixedUser = PrefixedKeys<User, 'user_'>;
// 类型:{ user_name: string; user_age: number; }
type UppercaseUser = UppercaseKeys<User>;
// 类型:{ NAME: string; AGE: number; }过滤函数类型
typescript
// 只保留函数类型的属性
type Methods<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
// 排除函数类型的属性
type NonMethods<T> = {
[K in keyof T as T[K] extends Function ? never : K]: T[K];
};
// 使用示例
interface User {
name: string;
age: number;
getName(): string;
getAge(): number;
}
type UserMethods = Methods<User>;
// 类型:{ getName(): string; getAge(): number; }
type UserData = NonMethods<User>;
// 类型:{ name: string; age: number; }值的转换
映射类型不仅可以修改键,还可以转换值的类型:
基本值转换
typescript
// 将所有属性值转换为可选
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// 将所有属性值转换为数组
type ArrayValues<T> = {
[K in keyof T]: T[K][];
};
// 将所有属性值转换为 Promise
type Promisify<T> = {
[K in keyof T]: Promise<T[K]>;
};
// 使用示例
interface User {
name: string;
age: number;
}
type NullableUser = Nullable<User>;
// 类型:{ name: string | null; age: number | null; }
type ArrayUser = ArrayValues<User>;
// 类型:{ name: string[]; age: number[]; }
type PromisifyUser = Promisify<User>;
// 类型:{ name: Promise<string>; age: Promise<number>; }条件值转换
typescript
// 将函数类型转换为返回 Promise 的函数
type AsyncMethods<T> = {
[K in keyof T]: T[K] extends (...args: infer P) => infer R
? (...args: P) => Promise<R>
: T[K];
};
// 将可选属性转换为必需,非可选属性转换为可选
type InvertOptional<T> = {
[K in keyof T]-?: T[K];
} & {
[K in keyof T]?: T[K];
};
// 使用示例
interface User {
name: string;
age?: number;
getName(): string;
}
type AsyncUser = AsyncMethods<User>;
// 类型:{
// name: string;
// age?: number;
// getName(): Promise<string>;
// }使用示例
示例 1:内置工具类型实现
映射类型是 TypeScript 内置工具类型的基础:
typescript
// Partial:所有属性变为可选
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Required:所有属性变为必需
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Readonly:所有属性变为只读
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Pick:选择特定属性
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Omit:排除特定属性
type Omit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
// 使用示例
interface User {
name: string;
age: number;
email: string;
}
type PartialUser = Partial<User>;
// 类型:{ name?: string; age?: number; email?: string; }
type UserName = Pick<User, 'name'>;
// 类型:{ name: string; }
type UserWithoutEmail = Omit<User, 'email'>;
// 类型:{ name: string; age: number; }示例 2:深度映射类型
映射类型可以递归应用于嵌套对象:
typescript
// 深度 Partial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// 深度 Readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// 深度 Required
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];
};
// 使用示例
interface User {
name: string;
profile: {
bio: string;
avatar?: string;
};
settings: {
theme: string;
notifications: {
email: boolean;
push?: boolean;
};
};
}
type DeepPartialUser = DeepPartial<User>;
// 类型:{
// name?: string;
// profile?: {
// bio?: string;
// avatar?: string;
// };
// settings?: {
// theme?: string;
// notifications?: {
// email?: boolean;
// push?: boolean;
// };
// };
// }示例 3:API 响应类型转换
typescript
// 将 API 请求类型转换为响应类型
type ApiRequest<T> = {
[K in keyof T]: T[K];
};
type ApiResponse<T> = {
[K in keyof T]: T[K] extends string
? string
: T[K] extends number
? number
: T[K] extends boolean
? boolean
: T[K];
};
// 添加时间戳和状态
type WithMetadata<T> = T & {
timestamp: number;
status: 'success' | 'error';
};
// 使用示例
interface CreateUserRequest {
name: string;
age: number;
email: string;
}
type CreateUserResponse = WithMetadata<CreateUserRequest>;
// 类型:{
// name: string;
// age: number;
// email: string;
// timestamp: number;
// status: 'success' | 'error';
// }示例 4:表单类型转换
typescript
// 将数据模型转换为表单类型(所有字段可选,添加错误信息)
type FormField<T> = {
value: T;
error?: string;
};
type FormData<T> = {
[K in keyof T]: FormField<T[K]>;
};
// 将表单类型转换回数据模型
type ExtractFormData<T> = {
[K in keyof T]: T[K] extends FormField<infer U> ? U : T[K];
};
// 使用示例
interface User {
name: string;
age: number;
email: string;
}
type UserForm = FormData<User>;
// 类型:{
// name: FormField<string>;
// age: FormField<number>;
// email: FormField<string>;
// }
// 使用示例
const userForm: UserForm = {
name: { value: 'John', error: undefined },
age: { value: 30, error: undefined },
email: { value: 'john@example.com', error: 'Invalid email' }
};示例 5:事件系统类型
typescript
// 将配置对象转换为事件映射
type EventMap<T> = {
[K in keyof T]: (payload: T[K]) => void;
};
// 将事件映射转换为事件发射器接口
type EventEmitter<T> = {
on<K extends keyof T>(event: K, handler: EventMap<T>[K]): void;
off<K extends keyof T>(event: K, handler: EventMap<T>[K]): void;
emit<K extends keyof T>(event: K, payload: T[K]): void;
};
// 使用示例
interface AppEvents {
userLogin: { userId: number; username: string };
userLogout: { userId: number };
messageReceived: { message: string; from: string };
}
type AppEventEmitter = EventEmitter<AppEvents>;
// 类型:{
// on<K extends keyof AppEvents>(event: K, handler: (payload: AppEvents[K]) => void): void;
// off<K extends keyof AppEvents>(event: K, handler: (payload: AppEvents[K]) => void): void;
// emit<K extends keyof AppEvents>(event: K, payload: AppEvents[K]): void;
// }示例 6:数据库模型转换
typescript
// 将数据库模型转换为 API 模型(排除内部字段)
type ApiModel<T> = Omit<T, 'id' | 'createdAt' | 'updatedAt' | 'deletedAt'>;
// 将 API 模型转换为创建模型(所有字段必需)
type CreateModel<T> = Required<ApiModel<T>>;
// 将 API 模型转换为更新模型(所有字段可选)
type UpdateModel<T> = Partial<ApiModel<T>>;
// 使用示例
interface DbUser {
id: number;
name: string;
age: number;
email: string;
password: string;
createdAt: Date;
updatedAt: Date;
deletedAt?: Date;
}
type UserApiModel = ApiModel<DbUser>;
// 类型:{ name: string; age: number; email: string; password: string; }
type CreateUser = CreateModel<DbUser>;
// 类型:{ name: string; age: number; email: string; password: string; }
type UpdateUser = UpdateModel<DbUser>;
// 类型:{ name?: string; age?: number; email?: string; password?: string; }示例 7:类型安全的配置合并
typescript
// 合并配置类型,后一个配置覆盖前一个
type Merge<T, U> = {
[K in keyof T | keyof U]: K extends keyof U
? U[K]
: K extends keyof T
? T[K]
: never;
};
// 深度合并
type DeepMerge<T, U> = {
[K in keyof T | keyof U]: K extends keyof U
? K extends keyof T
? T[K] extends object
? U[K] extends object
? DeepMerge<T[K], U[K]>
: U[K]
: U[K]
: U[K]
: K extends keyof T
? T[K]
: never;
};
// 使用示例
interface BaseConfig {
host: string;
port: number;
database: {
name: string;
pool: number;
};
}
interface OverrideConfig {
port: 8080;
database: {
pool: 20;
};
}
type MergedConfig = DeepMerge<BaseConfig, OverrideConfig>;
// 类型:{
// host: string;
// port: 8080;
// database: {
// name: string;
// pool: 20;
// };
// }映射类型与条件类型结合
映射类型经常与条件类型结合使用,实现更复杂的类型转换:
typescript
// 根据条件选择性地转换属性
type ConditionalMap<T> = {
[K in keyof T]: T[K] extends string
? T[K] | null
: T[K] extends number
? T[K] | 0
: T[K];
};
// 过滤并转换属性
type StringToNumber<T> = {
[K in keyof T as T[K] extends string ? K : never]: number;
};
// 使用示例
interface Mixed {
name: string;
age: number;
active: boolean;
}
type ConditionalMixed = ConditionalMap<Mixed>;
// 类型:{ name: string | null; age: number | 0; active: boolean; }
type StringKeysToNumber = StringToNumber<Mixed>;
// 类型:{ name: number; }类型检查示例
常见错误
typescript
// ❌ 错误:映射类型语法错误
// type Wrong = { [K in T]: T[K] }; // 缺少 keyof
// ❌ 错误:不能直接使用值类型作为键
// type Wrong<T> = { [K in T]: K }; // T 必须是对象类型
// ❌ 错误:修饰符语法错误
// type Wrong<T> = { [K in keyof T] readonly?: T[K] }; // 修饰符顺序错误正确写法
typescript
// ✅ 正确:基本映射类型
type Copy<T> = {
[K in keyof T]: T[K];
};
// ✅ 正确:添加修饰符
type Partial<T> = {
[K in keyof T]?: T[K];
};
// ✅ 正确:移除修饰符
type Required<T> = {
[K in keyof T]-?: T[K];
};
// ✅ 正确:过滤键
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// ✅ 正确:条件转换
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};注意事项
提示
- 映射类型使用
[K in keyof T]语法遍历类型的所有键 - 可以使用
as子句过滤和转换键名 - 修饰符
readonly和?可以通过-前缀移除 - 映射类型与条件类型结合使用可以实现复杂的类型转换
- 映射类型是构建工具类型的基础,理解映射类型对于掌握 TypeScript 高级特性非常重要
注意
- 映射类型只能应用于对象类型,不能应用于原始类型
- 映射类型会保留原类型的索引签名和可选属性标记
- 复杂的映射类型可能会影响类型检查性能
- 使用
as子句过滤键时,被过滤的键会变成never类型 - 映射类型中的条件类型判断基于类型关系,不是值的关系
重要
- 映射类型是 TypeScript 类型系统的核心特性,是构建工具类型的基础
- 理解映射类型的工作原理对于掌握 TypeScript 高级类型至关重要
- 映射类型与条件类型、泛型约束结合使用,可以构建强大的类型系统
- 映射类型不会影响运行时的性能,因为类型信息在编译时会被擦除
信息
映射类型的优势:
- 自动化类型转换:自动生成类型变体,减少重复代码
- 类型安全:在编译时确保类型转换的正确性
- 代码复用:创建可复用的类型工具
- 自文档化:类型定义本身就是文档
映射类型的应用场景:
- 工具类型实现(Partial、Required、Readonly、Pick、Omit 等)
- API 类型转换(请求类型、响应类型)
- 表单类型处理
- 配置对象转换
- 数据库模型转换
- 框架和库的类型定义
相关链接
- keyof 和 typeof 操作符 - 了解 keyof 操作符的使用
- 条件类型 - 学习条件类型与映射类型的结合使用
- 索引访问类型 - 了解如何访问类型的属性类型
- 工具类型 - 学习内置工具类型的实现原理
- 泛型 - 了解泛型的基础概念
- TypeScript 官方文档 - 映射类型