Skip to content
第 103 / 250 章前端⏱ 10 分钟阅读

第 103 章:映射类型(Mapped Types)

学习目标

  • 理解映射类型的本质和语法
  • 学会内置映射类型(Partial / Required / Readonly / Pick / Omit)
  • 掌握自定义映射类型
  • 理解键名重映射(Key Remapping)

一、什么是映射类型

映射类型是基于已有类型创建新类型的方式,遍历键并转换

1.1 第一个映射类型

typescript
type User = {
  name: string;
  age: number;
  email: string;
};

// 把所有字段变成可选
type OptionalUser = {
  [K in keyof User]?: User[K];
};
// 等价于
// {
//   name?: string;
//   age?: number;
//   email?: string;
// }

1.2 语法拆解

typescript
//           K 是键的占位符
//           ↓
{ [K in keyof T]: ... }
//        ↑
//        遍历 T 的所有键

// 类比 JS:
Object.keys(obj).forEach((key) => { /* 转换 */ });

二、内置映射类型

2.1 Partial<T> - 所有字段可选

typescript
type Partial<T> = {
  [K in keyof T]?: T[K];
};

interface User {
  id: number;
  name: string;
  email: string;
}

// 场景:更新时只传部分字段
function updateUser(id: number, data: Partial<User>) {
  // data 的每个字段都可选
}

updateUser(1, { name: 'Tom' });         // ✅
updateUser(1, { email: 'a@b.c' });      // ✅
updateUser(1, { name: 'Tom', email: 'a@b.c' });  // ✅

2.2 Required<T> - 所有字段必填

typescript
type Required<T> = {
  [K in keyof T]-?: T[K];
};

interface Config {
  host?: string;
  port?: number;
  debug?: boolean;
}

const config: Required<Config> = {
  host: 'localhost',  // 现在是必填
  port: 3000,         // 现在是必填
  debug: false        // 现在是必填
};

2.3 Readonly<T> - 所有字段只读

typescript
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

const user: Readonly<User> = {
  id: 1,
  name: 'Tom',
  email: 'tom@example.com'
};

user.name = 'Jerry';  // ❌ Cannot assign to 'name' because it is read-only

2.4 Pick<T, K> - 挑选字段

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

// 场景:从 User 中只取展示用字段
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: number; name: string }

const preview: UserPreview = { id: 1, name: 'Tom' };

2.5 Omit<T, K> - 排除字段

typescript
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

// 场景:创建用户时不传 id
type CreateUserDTO = Omit<User, 'id'>;
// { name: string; email: string }

const dto: CreateUserDTO = { name: 'Tom', email: 'tom@example.com' };

2.6 Record<K, V> - 构造字典

typescript
type Record<K extends keyof any, V> = {
  [P in K]: V;
};

// 场景:页面权限映射
type PagePermission = Record<'home' | 'profile' | 'admin', boolean>;
// { home: boolean; profile: boolean; admin: boolean }

const perms: PagePermission = {
  home: true,
  profile: false,
  admin: false
};

// 场景:动态键
type StringMap = Record<string, number>;
const map: StringMap = { a: 1, b: 2 };

2.7 Exclude / Extract

typescript
// Exclude: 从 T 中排除 U
type Exclude<T, U> = T extends U ? never : T;

type T1 = Exclude<'a' | 'b' | 'c', 'a'>;  // 'b' | 'c'

// Extract: 从 T 中提取 U
type Extract<T, U> = T extends U ? T : never;

type T2 = Extract<'a' | 'b' | 'c', 'a' | 'b'>;  // 'a' | 'b'

// 实战:过滤联合类型
type Status = 'pending' | 'success' | 'error' | 'idle';
type ActiveStatus = Exclude<Status, 'idle'>;  // 'pending' | 'success' | 'error'

2.8 NonNullable

typescript
type NonNullable<T> = T extends null | undefined ? never : T;

type T1 = NonNullable<string | null>;       // string
type T2 = NonNullable<number | undefined>;   // number
type T3 = NonNullable<null | undefined>;    // never

2.9 ReturnType / Parameters

typescript
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

function fetchUser(id: number, name: string): User {
  // ...
}

type Return = ReturnType<typeof fetchUser>;    // User
type Params = Parameters<typeof fetchUser>;    // [id: number, name: string]

三、自定义映射类型

3.1 把所有字段变成 nullable

typescript
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

type NullableUser = Nullable<User>;
// { id: number | null; name: string | null; email: string | null }

3.2 把所有字段变成 getter

typescript
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface User {
  name: string;
  age: number;
}

type UserGetters = Getters<User>;
// {
//   getName: () => string;
//   getAge: () => number;
// }

3.3 过滤指定字段

typescript
// 只保留 string 类型的字段
type StringFields<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface Mixed {
  id: number;
  name: string;
  email: string;
  age: number;
}

type OnlyString = StringFields<Mixed>;
// { name: string; email: string }

四、键名重映射(Key Remapping, TS 4.1+)

as 子句修改键名

4.1 添加前缀

typescript
type WithPrefix<T, P extends string> = {
  [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};

interface User {
  name: string;
  age: number;
}

type PrefixedUser = WithPrefix<User, 'user'>;
// { userName: string; userAge: number }

4.2 过滤键

typescript
type FilteredKeys<T, Filter extends string> = {
  [K in keyof T as K extends Filter ? K : never]: T[K];
};

interface User {
  id: number;
  name: string;
  email: string;
}

type OnlyName = FilteredKeys<User, 'name'>;  // { name: string }

4.3 移除特定字段

typescript
type RemoveField<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P];
};

type WithoutEmail = RemoveField<User, 'email'>;
// { id: number; name: string }

4.4 重写键名

typescript
type RenameKey<T, OldKey extends keyof T, NewKey extends string> = {
  [K in keyof T as K extends OldKey ? NewKey : K]: T[K];
};

type RenamedUser = RenameKey<User, 'name', 'fullName'>;
// { id: number; fullName: string; email: string }

五、深入理解映射修饰符

5.1 +?-?

typescript
// +? 加可选(默认就是 +?,所以写不写都行)
type WithOptional<T> = {
  [K in keyof T]+?: T[K];
};

// -? 移除可选
type RemoveOptional<T> = {
  [K in keyof T]-?: T[K];
};

5.2 +readonly-readonly

typescript
// +readonly 加只读
type AddReadonly<T> = {
  +readonly [K in keyof T]: T[K];
};

// -readonly 移除只读
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

interface Frozen {
  readonly id: number;
  readonly name: string;
}

type Unfrozen = Mutable<Frozen>;
// { id: number; name: string }  // 可写

六、映射类型与泛型结合

6.1 通用 CRUD 类型

typescript
interface BaseEntity {
  id: number;
  createdAt: Date;
  updatedAt: Date;
}

// 创建:不带 id 和时间戳
type CreateDTO<T> = Omit<T, 'id' | 'createdAt' | 'updatedAt'>;

// 更新:所有字段可选
type UpdateDTO<T> = Partial<Omit<T, 'id'>>;

// 列表查询:分页参数
type ListQuery<T> = Partial<T> & {
  page: number;
  pageSize: number;
};

// 使用
interface Article extends BaseEntity {
  title: string;
  content: string;
  authorId: number;
}

type CreateArticleDTO = CreateDTO<Article>;
type UpdateArticleDTO = UpdateDTO<Article>;
type ListArticleQuery = ListQuery<Article>;

6.2 通用响应包装

typescript
type ApiResponse<T> = {
  code: number;
  message: string;
  data: T;
};

type PagedResponse<T> = {
  code: number;
  data: {
    items: T[];
    total: number;
  };
};

七、实战案例

7.1 表单字段生成器

typescript
type FormField<T> = {
  [K in keyof T]: {
    value: T[K];
    error?: string;
    touched: boolean;
  };
};

interface LoginForm {
  username: string;
  password: string;
  remember: boolean;
}

type LoginFormState = FormField<LoginForm>;
// {
//   username: { value: string; error?: string; touched: boolean };
//   password: { value: string; error?: string; touched: boolean };
//   remember: { value: boolean; error?: string; touched: boolean };
// }

7.2 EventEmitter 类型

typescript
type EventMap = {
  click: { x: number; y: number };
  focus: { target: string };
  blur: undefined;
};

type EventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]?: (payload: T[K]) => void;
};

type Handlers = EventHandlers<EventMap>;
// {
//   onClick?: (payload: { x: number; y: number }) => void;
//   onFocus?: (payload: { target: string }) => void;
//   onBlur?: (payload: undefined) => void;
// }

7.3 Redux Action 类型

typescript
type Action<T, P = void> = P extends void
  ? { type: T }
  : { type: T; payload: P };

type ActionMap = {
  INCREMENT: number;
  SET_NAME: string;
  RESET: void;
};

type ActionUnion = {
  [K in keyof ActionMap]: Action<K, ActionMap[K]>
}[keyof ActionMap];

// 等价于:
// { type: 'INCREMENT'; payload: number }
// | { type: 'SET_NAME'; payload: string }
// | { type: 'RESET' }

八、常见错误

8.1 键名映射写错

typescript
// ❌ keyof T 不在键名位置
type Bad<T> = { [K in T]: ... };  // T 必须是联合类型

// ✅ 标准写法
type Good<T> = { [K in keyof T]: ... };

8.2 映射接口会报错

typescript
// ❌ interface 不支持映射
// interface Mapped<T> { [K in keyof T]: T[K] }  // ❌

// ✅ 必须用 type
type Mapped<T> = { [K in keyof T]: T[K] };

8.3 过度映射

typescript
// ❌ 不需要映射时硬用
type WrapString<T> = { value: T };  // 这不是映射

// ✅ 简单的就直接写
type WrapString = { value: string };

九、本章小结

要点关键
映射语法{ [K in keyof T]: T[K] }
Partial/Required可选/必填切换
Readonly只读
Pick/Omit挑选/排除字段
Record构造字典
键名重映射as 修改键名
修饰符+? -? +readonly -readonly

动手练习

  1. 自定义映射:写一个 Mutable<T> 类型,把 Readonly<T> 变成可写
  2. 键名转换:写一个 EventEmitter 的类型映射,把 'click' 转成 'onClick'
  3. 过滤映射:写一个 PickByType<T, V> 类型,只保留值为 V 类型的字段
  4. 递归映射:写一个 DeepReadonly<T>,让嵌套对象也变只读

推荐阅读


下一章第 104 章:条件类型(Conditional Types)

本站基于 VitePress 构建 · 由 Codebook 团队维护