TypeScript 映射类型

197 阅读5分钟

映射类型(Mapped Types) 是 TypeScript 中最强大的类型工具之一,它允许你基于现有类型创建新的类型结构。简单来说,它是一种能够遍历现有类型键并转换其属性的类型编程工具,可以让你像处理数据一样处理类型系统。

为什么需要映射类型?

想象你在处理用户数据时遇到这种情况:

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  createdAt: Date;
}

现在你需要:

  • 创建一个将所有属性设为可选的版本
  • 创建一个只读版本
  • 只选取部分属性构成新类型

映射类型让你能够以声明式的方式完成这些转换,而无需重复定义新类型。

映射类型基础语法

映射类型的核心语法如下:

type NewType = {
  [Key in KeyType]: ValueType;
};
  • Key:新类型的键(通常使用标识符 K
  • in:关键字,表示遍历操作
  • KeyType:要遍历的键的集合(常是 keyof T
  • ValueType:新类型中每个键对应的值类型

基本示例:克隆类型

type Clone<T> = {
  [P in keyof T]: T[P];
};

type UserClone = Clone<User>;
// 等同于:
// {
//   id: number;
//   name: string;
//   email: string;
//   age: number;
//   createdAt: Date;
// }

TypeScript 内置映射类型

TypeScript 提供了一套内置映射类型解决常见问题:

1. Partial<T> - 所有属性可选

type PartialUser = Partial<User>;
// 等同于:
// {
//   id?: number;
//   name?: string;
//   email?: string;
//   age?: number;
//   createdAt?: Date;
// }

2. Required<T> - 所有属性必选

type RequiredUser = Required<PartialUser>;
// 恢复为所有属性必选的User接口

3. Readonly<T> - 所有属性只读

type ReadonlyUser = Readonly<User>;
// 所有属性添加 readonly 修饰符

4. Pick<T, K> - 选取特定属性

type UserBasics = Pick<User, 'id' | 'name' | 'email'>;
// 等同于:
// {
//   id: number;
//   name: string;
//   email: string;
// }

5. Record<K, T> - 创建键值映射

type PageRecord = Record<'home' | 'about' | 'contact', string>;
// 等同于:
// {
//   home: string;
//   about: string;
//   contact: string;
// }

6. Omit<T, K> - 排除特定属性

type UserWithoutAge = Omit<User, 'age'>;
// 等同于:
// {
//   id: number;
//   name: string;
//   email: string;
//   createdAt: Date;
// }

理解映射类型的实现原理

了解内置类型的实现有助于我们理解映射类型的强大能力:

Partial 的实现

type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

Required 的实现

type MyRequired<T> = {
  [P in keyof T]-?: T[P];
};

Readonly 的实现

type MyReadonly<T> = {
  readonly [P in keyof T]: T[P];
};

Pick 的实现

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

映射修饰符:控制属性特性

通过 +- 修饰符,我们可以显式控制属性的特性:

// 移除只读并添加可选
type Mutable<T> = {
  -readonly [P in keyof T]?: T[P];
};

// 添加只读并移除可选
type ReadonlyRequired<T> = {
  +readonly [P in keyof T]-?: T[P];
};

键重映射(Key Remapping)

TypeScript 4.1 引入了键重映射,使用 as 关键字进行键的转换:

// 添加前缀
type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

type UserGetters = Getters<User>;
// 等同于:
// {
//   getId: () => number;
//   getName: () => string;
//   getEmail: () => string;
//   getAge: () => number;
//   getCreatedAt: () => Date;
// }

// 基于条件过滤属性
type NumericProps<T> = {
  [K in keyof T as T[K] extends number ? K : never]: T[K];
};

type UserNumbers = NumericProps<User>;
// {
//   id: number;
//   age: number;
// }

// 修改键名
type SnakeCase<T> = {
  [K in keyof T as `user_${string & K}`]: T[K];
};

映射类型的高级应用

1. 类型安全的表单处理

// 将接口转为表单字段配置
type FormFieldConfig<T> = {
  [K in keyof T]: {
    value: T[K];
    error?: string;
    touched: boolean;
  }
};

type UserForm = FormFieldConfig<User>;
// 每个字段都包含value、error和touched属性

// 提取表单值
type FormValues<T> = {
  [K in keyof T]: T[K]['value'];
};

const getUserValues = (form: UserForm): FormValues<UserForm> => {
  return Object.fromEntries(
    Object.entries(form).map(([key, field]) => [key, field.value])
  ) as FormValues<UserForm>;
};

2. 响应式属性自动生成

// Vue3风格响应式属性
type Reactive<T> = {
  [K in keyof T]: {
    value: T[K];
    // 响应式相关方法
    get(): T[K];
    set(value: T[K]): void;
  }
} & {
  $update(updater: (state: T) => void): void;
}

type ReactiveUser = Reactive<User>;

3. API 响应标准化

// 标准化API响应
type ApiResponse<T> = 
  | { status: 'loading' }
  | { status: 'success'; data: T; timestamp: Date }
  | { status: 'error'; code: number; message: string };

// 提取成功状态的数据类型
type SuccessData<T> = 
  T extends ApiResponse<infer D> ? D : never;

// 转换DTO到实体
type Entity<T> = T & { id: number; createdAt: Date };

type UserDTO = Pick<User, 'name' | 'email' | 'age'>;
type UserEntity = Entity<UserDTO>;

// API响应映射
type ApiResponseToEntity<T> = ApiResponse<Entity<T>>;

4. 组件Props动态生成

// 基于组件配置生成Props类型
interface ComponentConfig {
  size?: 'small' | 'medium' | 'large';
  variant?: 'primary' | 'secondary' | 'tertiary';
  disabled?: boolean;
}

// 动态创建组件Props
type CreateComponentProps<T extends ComponentConfig> = {
  [K in keyof T]: T[K]
} & {
  children?: React.ReactNode;
  onClick?: () => void;
}

// 使用示例
type ButtonProps = CreateComponentProps<{
  size: 'small' | 'medium' | 'large';
  variant: 'primary' | 'secondary';
}>;

5. 递归映射类型

映射类型可以递归处理嵌套结构:

// 将所有属性变为只读(包括嵌套对象)
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object 
    ? T[K] extends Function 
      ? T[K] 
      : DeepReadonly<T[K]> 
    : T[K];
};

// 将所有属性变为可选(包括嵌套对象)
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object 
    ? T[K] extends Function 
      ? T[K] 
      : DeepPartial<T[K]> 
    : T[K];
};

// 将所有Date转为string(序列化场景)
type Serialized<T> = {
  [K in keyof T]: T[K] extends Date 
    ? string 
    : T[K] extends object
      ? Serialized<T[K]>
      : T[K];
};

type SerializedUser = Serialized<User>;
// createdAt变为string类型

映射类型最佳实践

1. 合理使用组合

// 组合多个映射类型
type ReadonlyPartial<T> = Readonly<Partial<T>>;

// 更精确的控制
type SettableProps<T> = Pick<T, {
  [K in keyof T]: T[K] extends Function ? never : K 
}[keyof T]>;

2. 避免过度嵌套

当映射类型嵌套过深时,TypeScript 编译器可能会报错"类型实例化过深且可能无限":

// 优化前:可能导致递归过深
type DeepTransform<T> = ...

// 优化:添加终止条件
type SafeDeepTransform<T> = 
  T extends object
    ? { [K in keyof T]: SafeDeepTransform<T[K]> }
    : T;

3. 使用类型约束提高安全性

// 添加约束确保安全
type SafePick<T, K extends keyof T> = {
  [P in K]: T[P];
};

type SafeOmit<T, K extends keyof any> = 
  Pick<T, Exclude<keyof T, K>>;

4. 键重映射的妙用

// 从联合类型创建映射类型
type EventMap<EventType extends string> = {
  [E in EventType as `on${Capitalize<E>}`]: () => void;
};

type ClickEvents = EventMap<'click' | 'doubleClick'>;
// {
//   onClick: () => void;
//   onDoubleClick: () => void;
// }

// 过滤特定类型属性
type FunctionProps<T> = {
  [K in keyof T as T[K] extends Function ? K : never]: T[K];
};

5. 与条件类型结合

// 根据条件筛选属性
type NumericOrStringKeys<T> = {
  [K in keyof T]: T[K] extends number | string ? K : never;
}[keyof T];

type UserKeys = NumericOrStringKeys<User>; // "id" | "name" | "email" | "age"

// 条件转换属性值
type WrapNullable<T> = {
  [K in keyof T]: T[K] extends number | string 
    ? T[K] | null 
    : T[K];
};

映射类型的性能考量

映射类型虽强大,但过度使用可能影响类型检查性能:

  1. 避免深层递归:递归深度超过50层可能导致性能问题
  2. 简化复杂映射:拆分为多个简单步骤往往更好
  3. 使用类型缓存
// 缓存映射结果
type Precomputed<T> = { [K in keyof T]: SomeComplexType<T[K]> };

// 使用缓存
function process<T>(obj: T): Precomputed<T> {
  // ...
}

实际应用场景

1. Redux 状态管理

// 自动生成Action类型
type ActionCreators<T> = {
  [K in keyof T as T[K] extends (...args: any) => any 
    ? `set${Capitalize<string & K>}` 
    : never]: T[K] extends (state: any, action: any) => any 
        ? () => { type: string }
        : (payload: T[K]) => { type: string; payload: T[K] };
}

// 使用示例
type UserActions = ActionCreators<UserState>;
// {
//   setName: (payload: string) => { type: string; payload: string };
//   setEmail: (payload: string) => { type: string; payload: string };
//   // ...
// }

2. REST API 客户端生成

type ResourceEndpoints<T> = {
  list: () => Promise<T[]>;
  get: (id: string) => Promise<T>;
  create: (data: Omit<T, 'id'>) => Promise<T>;
  update: (id: string, data: Partial<T>) => Promise<T>;
  delete: (id: string) => Promise<void>;
}

function createResource<T>(resource: string): ResourceEndpoints<T> {
  return {
    list: () => fetch(`/api/${resource}`).then(res => res.json()),
    // 其他方法实现...
  }
}

const userApi = createResource<User>('users');
userApi.list(); // 返回Promise<User[]>

3. 表单验证架构

// 验证规则映射
type ValidationRules<T> = {
  [K in keyof T]?: (value: T[K]) => string | null;
}

// 表单错误映射
type FieldErrors<T> = {
  [K in keyof T]?: string;
}

// 使用示例
const userRules: ValidationRules<User> = {
  name: (value) => !value ? '名称不能为空' : null,
  email: (value) => 
    !/^\S+@\S+\.\S+$/.test(value) ? '邮箱格式不正确' : null,
};

function validate<T>(obj: T, rules: ValidationRules<T>): FieldErrors<T> {
  const errors: FieldErrors<T> = {};
  for (const key in rules) {
    if (rules[key]) {
      const error = rules[key]!(obj[key]);
      if (error) (errors as any)[key] = error;
    }
  }
  return errors;
}

映射类型的作用

映射类型让 TypeScript 开发者能够:

  • 🔑 基于现有类型动态生成新类型
  • 🧩 通过组合创建复杂类型系统
  • ⚙️ 自动化重复的类型定义
  • 🛡 增强类型安全性和代码健壮性
  • 🔄 实现类型驱动开发模式

正如 TypeScript 负责人 Anders Hejlsberg 所说:"映射类型将类型系统从静态定义提升到了动态运算的层次"。掌握映射类型,你将在以下场景中游刃有余:

  1. 处理DTO转换和序列化
  2. 构建灵活的状态管理系统
  3. 创建类型安全的API客户端
  4. 设计动态表单验证
  5. 开发可复用的组件库
  6. 实现复杂的企业级应用架构

"优秀的类型设计不是简单地描述数据,而是表达数据之间的关系和转换规则。映射类型正是实现这一目标的核心工具。" — TypeScript 高级模式