跳到主要内容

实现 Pick/Omit/Exclude/Extract

问题

如何手动实现 TypeScript 内置的 PickOmitExcludeExtract 等工具类型?

答案

这些工具类型是 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 }

相关链接