Skip to content

映射类型

概述

映射类型(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];
};

映射类型让我们能够:

  1. 自动生成类型变体:基于现有类型自动创建新的类型
  2. 批量修改属性:一次性修改类型的所有属性
  3. 创建类型工具:构建可复用的类型转换工具
  4. 类型级编程:在类型层面实现数据转换逻辑

基本语法

基本映射类型

映射类型使用 [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 类型转换(请求类型、响应类型)
  • 表单类型处理
  • 配置对象转换
  • 数据库模型转换
  • 框架和库的类型定义

相关链接

基于 VitePress 构建