实现 Pick/Omit/Exclude/Extract
问题
如何手动实现 TypeScript 内置的 Pick、Omit、Exclude、Extract 等工具类型?
答案
这些工具类型是 TypeScript 类型系统的基础组件,理解它们的实现原理有助于掌握映射类型和条件类型的应用。
Pick 实现
Pick<T, K> 从类型 T 中选择属性 K 组成新类型:
// 内置实现
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// 原理解析
// 1. K extends keyof T: K 必须是 T 的键的子集
// 2. [P in K]: 遍历 K 中的每个键
// 3. T[P]: 获取 T 中键 P 对应的值类型
使用示例
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// 选择公开字段
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// { id: number; name: string; email: string }
// 用于 API 返回
function getPublicProfile(user: User): Pick<User, 'id' | 'name'> {
return { id: user.id, name: user.name };
}
扩展:PickByType
选择特定类型的属性:
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Example {
name: string;
age: number;
active: boolean;
email: string;
}
type StringProps = PickByType<Example, string>;
// { name: string; email: string }
Omit 实现
Omit<T, K> 从类型 T 中排除属性 K:
// 方式一:使用 Pick 和 Exclude
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// 方式二:直接使用映射类型(TS 4.1+)
type Omit2<T, K extends keyof any> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
// 原理解析
// 方式一:
// 1. Exclude<keyof T, K>: 从 T 的键中排除 K
// 2. Pick<T, ...>: 选择剩余的键
// 方式二:
// 1. P extends K ? never : P: 如果 P 在 K 中,映射为 never(被过滤)
// 2. 否则保留 P
使用示例
interface User {
id: number;
name: string;
email: string;
password: string;
}
// 排除敏感字段
type SafeUser = Omit<User, 'password'>;
// { id: number; name: string; email: string }
// 创建用户时不需要 id
type CreateUserDto = Omit<User, 'id'>;
// { name: string; email: string; password: string }
// 排除多个
type BasicInfo = Omit<User, 'id' | 'password'>;
// { name: string; email: string }
扩展:OmitByType
排除特定类型的属性:
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;
}
type NoFunctions = OmitByType<Example, Function>;
// { name: string; age: number; active: boolean }
Exclude 实现
Exclude<T, U> 从联合类型 T 中排除可赋值给 U 的类型:
// 内置实现
type Exclude<T, U> = T extends U ? never : T;
// 原理解析
// 利用条件类型的分发特性
// 当 T 是联合类型时,会逐个应用条件判断
分发过程详解
type Result = Exclude<'a' | 'b' | 'c', 'a'>;
// 分发过程:
// = ('a' extends 'a' ? never : 'a')
// | ('b' extends 'a' ? never : 'b')
// | ('c' extends 'a' ? never : 'c')
// = never | 'b' | 'c'
// = 'b' | 'c'
使用示例
type AllEvents = 'click' | 'focus' | 'blur' | 'mouseover' | 'mouseout';
type MouseEvents = 'click' | 'mouseover' | 'mouseout';
type KeyboardEvents = Exclude<AllEvents, MouseEvents>;
// 'focus' | 'blur'
// 排除 null 和 undefined
type Primitive = string | number | boolean | null | undefined;
type NonNullPrimitive = Exclude<Primitive, null | undefined>;
// string | number | boolean
Extract 实现
Extract<T, U> 从联合类型 T 中提取可赋值给 U 的类型:
// 内置实现
type Extract<T, U> = T extends U ? T : never;
// 与 Exclude 相反
// Exclude: 不匹配的保留
// Extract: 匹配的保留
使用示例
type AllTypes = string | number | (() => void) | { name: string };
// 提取函数类型
type Functions = Extract<AllTypes, Function>;
// () => void
// 提取原始类型
type Primitives = Extract<AllTypes, string | number>;
// string | number
// 提取对象类型
type Objects = Extract<AllTypes, object>;
// (() => void) | { name: string }
Extract 应用:提取对象的方法名
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];
interface Example {
name: string;
age: number;
greet(): void;
update(data: any): Promise<void>;
}
type Methods = FunctionPropertyNames<Example>;
// 'greet' | 'update'
// 等价于
type Methods2 = Extract<keyof Example, 'greet' | 'update'>;
// 但这需要预知方法名
组合使用
实现 PartialByKeys
部分属性可选:
type PartialByKeys<T, K extends keyof T = keyof T> =
Omit<T, K> & Partial<Pick<T, K>> extends infer O
? { [P in keyof O]: O[P] }
: never;
interface User {
id: number;
name: string;
email: string;
}
type UserWithOptionalEmail = PartialByKeys<User, 'email'>;
// { id: number; name: string; email?: string }
实现 RequiredByKeys
部分属性必选:
type RequiredByKeys<T, K extends keyof T = keyof T> =
Omit<T, K> & Required<Pick<T, K>> extends infer O
? { [P in keyof O]: O[P] }
: never;
interface Config {
host?: string;
port?: number;
timeout?: number;
}
type ConfigWithRequiredHost = RequiredByKeys<Config, 'host'>;
// { host: string; port?: number; timeout?: number }
实现 ReadonlyByKeys
部分属性只读:
type ReadonlyByKeys<T, K extends keyof T = keyof T> =
Omit<T, K> & Readonly<Pick<T, K>> extends infer O
? { [P in keyof O]: O[P] }
: never;
interface User {
id: number;
name: string;
email: string;
}
type UserWithReadonlyId = ReadonlyByKeys<User, 'id'>;
// { readonly id: number; name: string; email: string }
高级实现
DeepPick
深度选择嵌套属性:
type DeepPick<T, K extends string> = K extends `${infer First}.${infer Rest}`
? First extends keyof T
? { [P in First]: DeepPick<T[First], Rest> }
: never
: K extends keyof T
? { [P in K]: T[K] }
: never;
interface Nested {
user: {
profile: {
name: string;
avatar: string;
};
settings: {
theme: string;
};
};
}
type PickedName = DeepPick<Nested, 'user.profile.name'>;
// { user: { profile: { name: string } } }
MutableKeys / ReadonlyKeys
获取可变/只读键:
// 获取可变键
type MutableKeys<T> = {
[K in keyof T]-?: (<U>() => U extends { [P in K]: T[K] } ? 1 : 2) extends
(<U>() => U extends { -readonly [P in K]: T[K] } ? 1 : 2)
? K
: never;
}[keyof T];
// 获取只读键
type ReadonlyKeys<T> = {
[K in keyof T]-?: (<U>() => U extends { [P in K]: T[K] } ? 1 : 2) extends
(<U>() => U extends { -readonly [P in K]: T[K] } ? 1 : 2)
? never
: K;
}[keyof T];
interface Example {
readonly id: number;
name: string;
readonly createdAt: Date;
email: string;
}
type Mutable = MutableKeys<Example>; // 'name' | 'email'
type ReadOnly = ReadonlyKeys<Example>; // 'id' | 'createdAt'
常见面试问题
Q1: 实现 Pick 的关键点是什么?
答案:
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
// 关键点:
// 1. 泛型约束:K extends keyof T
// 确保 K 只能是 T 的键的子集,否则编译错误
// 2. 映射类型:[P in K]
// 遍历 K 中的每个键,而不是 keyof T
// 3. 索引访问:T[P]
// 获取原类型中对应键的值类型
// 验证约束
interface User {
id: number;
name: string;
}
type Valid = MyPick<User, 'id'>; // OK
// type Invalid = MyPick<User, 'unknown'>; // 错误:约束不满足
Q2: Omit 的两种实现方式有什么区别?
答案:
// 方式一:组合 Pick 和 Exclude
type Omit1<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// 方式二:使用 as 子句(TS 4.1+)
type Omit2<T, K extends keyof any> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
// 区别:
// 1. 方式一:依赖其他工具类型,可读性好
// 2. 方式二:直接实现,性能略优
// 注意 K 的约束是 keyof any 而不是 keyof T
// 这允许排除不存在的键(不会报错)
type Test1 = Omit1<{ a: 1 }, 'a' | 'b'>; // {}
type Test2 = Omit2<{ a: 1 }, 'a' | 'b'>; // {}
// 如果约束为 keyof T,则排除不存在的键会报错
type StrictOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// type Test3 = StrictOmit<{ a: 1 }, 'a' | 'b'>; // 错误
Q3: 为什么 Exclude 能实现"排除"效果?
答案:
type Exclude<T, U> = T extends U ? never : T;
// 关键是条件类型的"分发"特性
// 当 T 是联合类型时,条件类型会对每个成员单独应用
// 详细过程
type Result = Exclude<'a' | 'b' | 'c', 'a' | 'b'>;
// 分发:
// = ('a' extends 'a' | 'b' ? never : 'a')
// | ('b' extends 'a' | 'b' ? never : 'b')
// | ('c' extends 'a' | 'b' ? never : 'c')
// 计算:
// = never | never | 'c'
// 简化(never 在联合中被忽略):
// = 'c'
// 如果要阻止分发:
type ExcludeNoDistribute<T, U> = [T] extends [U] ? never : T;
// 结果不同,作为整体判断
Q4: 如何实现类型安全的"Pick by value type"?
答案:
// 方式一:使用 as 子句过滤(推荐)
type PickByType<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
// 方式二:先获取键,再 Pick
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
type PickByType2<T, V> = Pick<T, KeysOfType<T, V>>;
// 测试
interface Example {
name: string;
age: number;
active: boolean;
email: string;
callback: () => void;
}
type StringProps = PickByType<Example, string>;
// { name: string; email: string }
type NumberProps = PickByType<Example, number>;
// { age: number }
type FunctionProps = PickByType<Example, Function>;
// { callback: () => void }
Q5: 实现 Diff 类型(两个类型的差集)
答案:
// 键的差集
type Diff<T, U> = Omit<T, keyof U> & Omit<U, keyof T>;
interface A {
a: string;
b: number;
c: boolean;
}
interface B {
b: number;
c: boolean;
d: Date;
}
type DiffAB = Diff<A, B>;
// { a: string } & { d: Date }
// 相当于 { a: string; d: Date }
// 对称差集(只在一方存在的键)
type SymmetricDiff<T, U> =
| Exclude<keyof T, keyof U>
| Exclude<keyof U, keyof T>;
type SymKeys = SymmetricDiff<A, B>; // 'a' | 'd'
// 交集(两方都存在的键)
type Intersection<T, U> = Pick<T, Extract<keyof T, keyof U>>;
type Common = Intersection<A, B>;
// { b: number; c: boolean }