内置的 Partial<T> 会把所有属性都变成可选,但实际业务中我们经常只需要把"某些指定的 key"变成可选,其余保持不变。比如创建用户时 id、createdAt 是后端生成的不需要传,其余字段必填;更新用户时某些字段可选、某些字段必填。PartialByKeys 通过交叉类型 + Pick/Omit 的组合,精确控制哪些 key 变可选,粒度比 Partial 更细,是表单提交、DTO 设计中的高频工具类型。
// PartialByKeys:按指定的 key 将对象属性设为可选,其余保持不变
// 比 Partial 更精细,常用于 DTO、表单提交、配置合并等场景
// 1. 实现方式一:Pick 可选部分 + Omit 保留部分,然后交叉
type PartialByKeys<T, K extends keyof T> =
Partial<Pick<T, K>> & Omit<T, K>;
// 2. 实现方式二:映射类型 + 条件类型(更直观)
type PartialByKeysV2<T, K extends keyof T> = {
[P in keyof T as P extends K ? P : P]?: T[P];
} & {
[P in keyof T as P extends K ? never : P]: T[P];
};
// 3. 简化版:直接用映射类型 + 条件可选
type PartialByKeysV3<T, K extends keyof T = keyof T> = {
[P in keyof T]?: P extends K ? T[P] : T[P];
} extends infer O
? { [P in keyof O]: O[P] }
: never;
// 4. 推荐实现:交叉 + 展开合并(最简洁高效)
type Simplify<T> = { [K in keyof T]: T[K] };
type PartialByKeysFinal<T, K extends keyof T> =
Simplify<Partial<Pick<T, K>> & Omit<T, K>>;
// 5. 业务示例:用户实体
interface User {
id: number;
name: string;
email: string;
age: number;
avatar: string;
createdAt: string;
updatedAt: string;
}
// 6. 创建用户 DTO:id、createdAt、updatedAt 可选(后端生成)
type CreateUserDto = PartialByKeysFinal<User, 'id' | 'createdAt' | 'updatedAt'>;
// 7. 更新用户 DTO:只有 name/email/age/avatar 可选,id 必填
type UpdateUserDto = PartialByKeysFinal<User, 'name' | 'email' | 'age' | 'avatar' | 'createdAt' | 'updatedAt'>;
// 8. 使用示例
const createData: CreateUserDto = {
name: 'Alice',
email: 'alice@example.com',
age: 25,
avatar: 'avatar.png',
// id、createdAt、updatedAt 可以不传
};
const updateData: UpdateUserDto = {
id: 1, // id 必填(不在 K 中)
name: 'Bob', // name 可选,可以只传部分字段
// 其他字段都可选
};
Partial<Pick<T, K>> & Omit<T, K>,利用内置工具类型组合实现,简洁易读。A & B 在 IDE 中显示不够直观,用 { [K in keyof T]: T[K] } 重新映射一遍,可以把交叉后的属性"展平"成一个对象的显示效果,更友好。keyof T,不传 K 时就等价于内置 Partial,行为一致、兼容性好,是工具类型设计的常见最佳实践。K extends keyof T 保证传入的 key 一定存在于 T 中,写错 key 名 TS 直接报错,类型安全有保障。// CreateUserDto 推导结果
type CreateResult = CreateUserDto;
// => {
// id?: number | undefined;
// createdAt?: string | undefined;
// updatedAt?: string | undefined;
// name: string;
// email: string;
// age: number;
// avatar: string;
// }
// UpdateUserDto 推导结果
type UpdateResult = UpdateUserDto;
// => {
// id: number; // 必填(不在 K 中)
// name?: string; // 可选
// email?: string; // 可选
// age?: number; // 可选
// avatar?: string; // 可选
// createdAt?: string; // 可选
// updatedAt?: string; // 可选
// }
// 不传 K 等价于 Partial
type AllOptional = PartialByKeysFinal<User, keyof User>;
// => 所有属性都可选
// key 不存在会报错
// type Bad = PartialByKeysFinal<User, 'notExist'>; // 错误
// 实战:表单提交,部分字段可空
interface FormData {
username: string;
password: string;
confirmPassword: string;
captcha: string;
remember: boolean;
}
type LoginForm = PartialByKeysFinal<FormData, 'remember' | 'captcha'>;
// username、password 必填;remember、captcha 可选