··2 阅读·预计 12 分钟
TypeScript 类型体操实战:手把手实现 DeepPartial 和 DeepRequired
日常开发中,我们常用 Partial<T> 和 Required<T> 来做对象类型的可选/必选转换。但它们只作用于第一层属性,嵌套对象就无能为力了。今天来手写 DeepPartial 和 DeepRequired,顺便聊聊递归类型、条件类型、映射类型这几个 TypeScript 类型系统的核心武器。
问题场景
interface Config {
db: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
cache: {
ttl: number;
strategy: 'memory' | 'redis';
};
}
// Partial 只影响第一层
type ShallowPartial = Partial<Config>;
// {
// db?: { host: string; port: number; credentials: { ... } };
// cache?: { ttl: number; strategy: 'memory' | 'redis' };
// }
// db.credentials 里的字段还是必选的!
// 我们希望的是:所有层级都变成可选
实现 DeepPartial
核心思路:递归 + 条件类型。
type DeepPartial<T> = T extends object
? {
[K in keyof T]?: DeepPartial<T[K]>;
}
: T;
解析:
T extends object:判断当前类型是否是对象类型[K in keyof T]?::遍历所有属性,加?使其可选DeepPartial<T[K]>:对每个属性值递归处理: T:非对象类型(string、number、boolean 等)直接返回
验证效果:
type DeepPartialConfig = DeepPartial<Config>;
// {
// db?: {
// host?: string;
// port?: number;
// credentials?: {
// username?: string;
// password?: string;
// };
// };
// cache?: {
// ttl?: number;
// strategy?: 'memory' | 'redis';
// };
// }
完美!所有层级都变成可选了。
实现 DeepRequired
反向操作,把所有可选属性变必选:
type DeepRequired<T> = T extends object
? {
[K in keyof T]-?: DeepRequired<T[K]>;
}
: T;
关键区别:-? 表示移除可选标记(removes the optional modifier)。
type ConfigWithOptionals = {
db?: {
host?: string;
port?: number;
};
cache?: {
ttl?: number;
strategy?: 'memory' | 'redis';
};
};
type StrictConfig = DeepRequired<ConfigWithOptionals>;
// {
// db: {
// host: string;
// port: number;
// };
// cache: {
// ttl: number;
// strategy: 'memory' | 'redis';
// };
// }
处理边界情况
1. 排除数组和 Map 等内置对象
原版实现会把 string[] 变成 { [key: number]: DeepPartial<string> },这不对。需要排除:
type DeepPartial<T> = T extends Function
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends Map<infer K, infer V>
? Map<K, DeepPartial<V>>
: T extends Set<infer V>
? Set<DeepPartial<V>>
: T extends object
? {
[K in keyof T]?: DeepPartial<T[K]>;
}
: T;
2. 排除 class 实例
对于 Date、RegExp 等内置类,不应递归展开:
type IsBuiltIn<T> = T extends Date | RegExp | Error | Function ? true : false;
type DeepPartial<T> = IsBuiltIn<T> extends true
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends object
? {
[K in keyof T]?: DeepPartial<T[K]>;
}
: T;
实战应用:DeepMerge 类型
在实际项目中,我们经常需要合并配置对象。实现一个类型安全的 deepMerge:
type DeepMerge<T, U> = {
[K in keyof T | keyof U]: K extends keyof U
? K extends keyof T
? T[K] extends object
? U[K] extends object
? DeepMerge<T[K], U[K]>
: U[K]
: U[K]
: U[K]
: K extends keyof T
? T[K]
: never;
};
// 使用示例
type BaseConfig = {
db: { host: string; port: number };
log: { level: 'info' | 'debug' };
};
type UserConfig = {
db: { port: number; pool: number };
log: { format: 'json' | 'text' };
};
type Merged = DeepMerge<BaseConfig, UserConfig>;
// {
// db: { host: string; port: number; pool: number };
// log: { level: 'info' | 'debug'; format: 'json' | 'text' };
// }
配合运行时的 deepMerge 函数:
function deepMerge<T extends object, U extends object>(
target: T,
source: U
): DeepMerge<T, U> {
const result = { ...target } as any;
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
const val = source[key];
if (val && typeof val === 'object' && !Array.isArray(val)) {
result[key] = deepMerge(result[key] || {}, val);
} else {
result[key] = val;
}
}
}
return result;
}
// 类型安全的配置合并
const base: BaseConfig = {
db: { host: 'localhost', port: 5432 },
log: { level: 'info' },
};
const user: UserConfig = {
db: { port: 5433, pool: 10 },
log: { format: 'json' },
};
const config = deepMerge(base, user);
// config.db.host ✅ 来自 base
// config.db.port ✅ 被 user 覆盖
// config.db.pool ✅ 来自 user
// config.log.level ✅ 来自 base
// config.log.format ✅ 来自 user
性能提示
TypeScript 的递归类型有深度限制(默认约 50 层),超过会报 Type instantiation is excessively deep and possibly infinite。
处理方式:限制递归深度
type DeepPartial<T, Depth extends number = 5> = Depth extends 0
? T
: T extends object
? {
[K in keyof T]?: DeepPartial<T[K], Prev[Depth]>;
}
: T;
type Prev = [never, 0, 1, 2, 3, 4, 5];
这样可以限制递归深度,避免编译器卡死。
总结
- Partial / Required — 第一层可选/必选,语法
?/-? - DeepPartial / DeepRequired — 递归可选/必选,递归 + 条件类型
- DeepMerge — 深度合并,联合类型 + 条件 + 递归
类型体操不是炫技,而是在编译期就捕获错误,减少运行时 bug。掌握递归类型、条件类型、映射类型这三板斧,能覆盖 90% 的业务场景。
0 评论
评论区
登录 后参与评论