映射类型
问题
什么是 TypeScript 映射类型?如何使用 keyof、in、as 来创建和转换类型?
答案
映射类型基于现有类型创建新类型,通过遍历键来转换每个属性。语法形式为 { [P in K]: T },其中 P 遍历 K 中的每个键。
基础语法
// 基本形式
type MappedType<T> = {
[P in keyof T]: T[P];
};
// 创建只读类型
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// 创建可选类型
type Partial<T> = {
[P in keyof T]?: T[P];
};
// 示例
interface User {
id: number;
name: string;
email: string;
}
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; readonly email: string }
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string }
键的遍历与修饰符
keyof 操作符
interface Person {
name: string;
age: number;
location: string;
}
type PersonKeys = keyof Person; // 'name' | 'age' | 'location'
// 与索引签名
interface StringMap {
[key: string]: any;
}
type StringMapKeys = keyof StringMap; // string | number
// 与数组
type ArrayKeys = keyof string[]; // number | 'length' | 'push' | ...
修饰符添加与移除
// 添加修饰符
type ReadonlyAll<T> = {
readonly [P in keyof T]: T[P];
};
type OptionalAll<T> = {
[P in keyof T]?: T[P];
};
// 移除修饰符(使用 - 前缀)
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
type Required<T> = {
[P in keyof T]-?: T[P];
};
// 示例
interface Config {
readonly host: string;
readonly port: number;
timeout?: number;
}
type MutableConfig = Mutable<Config>;
// { host: string; port: number; timeout?: number }
type RequiredConfig = Required<Config>;
// { readonly host: string; readonly port: number; timeout: number }
键重映射(as 子句)
TypeScript 4.1+ 支持使用 as 子句重新映射键:
基本键转换
// 添加前缀
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
// 添加后缀
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type PersonSetters = Setters<Person>;
// { setName: (value: string) => void; setAge: (value: number) => void }
过滤键
// 移除指定键
type RemoveKey<T, K> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
type WithoutAge = RemoveKey<Person, 'age'>;
// { name: string }
// 只保留特定类型的属性
type OnlyStringProps<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 = OnlyStringProps<Mixed>;
// { name: string; email: string }
// 只保留方法
type OnlyMethods<T> = {
[K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
};
类型转换组合
// EventEmitter 类型
type EventMap = {
click: { x: number; y: number };
focus: { target: HTMLElement };
blur: void;
};
type EventHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (
event: T[K]
) => void;
};
type Handlers = EventHandlers<EventMap>;
// {
// onClick: (event: { x: number; y: number }) => void;
// onFocus: (event: { target: HTMLElement }) => void;
// onBlur: (event: void) => void;
// }
常用映射类型模式
Record 类型
type Record<K extends keyof any, T> = {
[P in K]: T;
};
// 使用
type PageInfo = Record<'home' | 'about' | 'contact', { title: string }>;
const pages: PageInfo = {
home: { title: 'Home' },
about: { title: 'About' },
contact: { title: 'Contact' }
};
Pick 和 Omit
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Omit<T, K extends keyof any> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
// 或使用 Pick 和 Exclude
type Omit2<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
属性类型转换
// 所有属性变为 Promise
type Promised<T> = {
[K in keyof T]: Promise<T[K]>;
};
interface Data {
users: User[];
posts: Post[];
}
type AsyncData = Promised<Data>;
// { users: Promise<User[]>; posts: Promise<Post[]> }
// 所有属性变为可空
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// 所有属性变为数组
type ArrayOfProps<T> = {
[K in keyof T]: T[K][];
};
递归映射类型
深度只读
type DeepReadonly<T> = T extends (...args: any[]) => any
? T // 函数不处理
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
interface Nested {
a: {
b: {
c: string;
};
};
fn: () => void;
}
type DeepReadonlyNested = DeepReadonly<Nested>;
// {
// readonly a: {
// readonly b: {
// readonly c: string;
// };
// };
// readonly fn: () => void;
// }
深度可选
type DeepPartial<T> = T extends (...args: any[]) => any
? T
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
深度必选
type DeepRequired<T> = T extends (...args: any[]) => any
? T
: T extends object
? { [K in keyof T]-?: DeepRequired<T[K]> }
: T;
高级映射技巧
条件属性修饰
// 只读特定属性
type ReadonlyKeys<T, K extends keyof T> = {
readonly [P in K]: T[P];
} & {
[P in Exclude<keyof T, K>]: T[P];
};
interface User {
id: number;
name: string;
email: string;
}
type UserWithReadonlyId = ReadonlyKeys<User, 'id'>;
// { readonly id: number } & { name: string; email: string }
// 更优雅的实现
type ReadonlySome<T, K extends keyof T> = Omit<T, K> & Readonly<Pick<T, K>>;
基于值类型的条件
// 将所有字符串属性变为可选
type OptionalStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]?: T[K];
} & {
[K in keyof T as T[K] extends string ? never : K]: T[K];
};
// 将函数属性变为异步
type AsyncMethods<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => Promise<Awaited<R>>
: T[K];
};
interface API {
getUser(id: number): User;
getUsers(): User[];
readonly version: string;
}
type AsyncAPI = AsyncMethods<API>;
// {
// getUser(id: number): Promise<User>;
// getUsers(): Promise<User[]>;
// readonly version: string;
// }
模板字符串映射
// CSS 属性映射
type CSSProperties = {
marginTop: string;
marginBottom: string;
paddingLeft: string;
paddingRight: string;
};
// 驼峰转连字符
type CamelToKebab<S extends string> = S extends `${infer First}${infer Rest}`
? First extends Uppercase<First>
? `-${Lowercase<First>}${CamelToKebab<Rest>}`
: `${First}${CamelToKebab<Rest>}`
: S;
type KebabCSSProperties = {
[K in keyof CSSProperties as CamelToKebab<K & string>]: CSSProperties[K];
};
// {
// 'margin-top': string;
// 'margin-bottom': string;
// 'padding-left': string;
// 'padding-right': string;
// }
实际应用场景
表单状态管理
interface FormFields {
username: string;
email: string;
age: number;
}
// 表单状态
type FormState<T> = {
values: T;
errors: { [K in keyof T]?: string };
touched: { [K in keyof T]?: boolean };
dirty: { [K in keyof T]?: boolean };
};
// 表单验证器
type FormValidators<T> = {
[K in keyof T]?: (value: T[K], values: T) => string | undefined;
};
// 表单更新函数
type FormSetters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
API 响应转换
interface APIResponse<T> {
data: T;
status: number;
message: string;
}
// 批量包装 API 响应
type WrappedAPI<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => Promise<APIResponse<Awaited<R>>>
: T[K];
};
interface UserService {
getUser(id: number): User;
createUser(data: CreateUserDto): User;
}
type WrappedUserService = WrappedAPI<UserService>;
// {
// getUser(id: number): Promise<APIResponse<User>>;
// createUser(data: CreateUserDto): Promise<APIResponse<User>>;
// }
常见面试问题
Q1: 实现 Readonly 和 Mutable
答案:
// Readonly:添加 readonly 修饰符
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
// Mutable:移除 readonly 修饰符
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// 测试
interface Example {
readonly a: string;
b: number;
}
type Mut = Mutable<Example>;
// { a: string; b: number }
type RO = MyReadonly<Example>;
// { readonly a: string; readonly b: number }
Q2: 实现 Partial 和 Required
答案:
// Partial:添加可选修饰符
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
// Required:移除可选修饰符
type MyRequired<T> = {
[P in keyof T]-?: T[P];
};
// 测试
interface Example {
a: string;
b?: number;
}
type Part = MyPartial<Example>;
// { a?: string; b?: number }
type Req = MyRequired<Example>;
// { a: string; b: number }
Q3: 如何使用 as 子句过滤属性?
答案:
// 移除指定键
type OmitByKey<T, K extends keyof any> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
// 只保留特定类型的属性
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
// 排除特定类型的属性
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K];
};
// 测试
interface Example {
name: string;
age: number;
active: boolean;
callback: () => void;
}
// 只保留 string 属性
type StringOnly = PickByType<Example, string>;
// { name: string }
// 排除函数属性
type NoFunctions = OmitByType<Example, Function>;
// { name: string; age: number; active: boolean }
Q4: 如何实现属性名转换?
答案:
// Getter 风格
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// Setter 风格
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (val: T[K]) => void;
};
// 完整的 getter/setter
type GetterSetters<T> = Getters<T> & Setters<T>;
// 添加前缀
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};
// 测试
interface State {
count: number;
name: string;
}
type StateGetters = Getters<State>;
// { getCount: () => number; getName: () => string }
type PrefixedState = Prefixed<State, 'user'>;
// { userCount: number; userName: string }
Q5: 如何递归处理嵌套类型?
答案:
// 深度 Readonly
type DeepReadonly<T> = T extends (...args: any[]) => any
? T // 函数保持原样
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
// 深度 Partial
type DeepPartial<T> = T extends (...args: any[]) => any
? T
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
// 深度 Required
type DeepRequired<T> = T extends (...args: any[]) => any
? T
: T extends object
? { [K in keyof T]-?: DeepRequired<T[K]> }
: T;
// 测试
interface Nested {
a: {
b?: {
c: string;
};
};
}
type DeepReq = DeepRequired<Nested>;
// { a: { b: { c: string } } }
// 所有层级的 ? 都被移除