TypeScript 如何做深度 Partial
你肯定碰过这种情况:写代码时想把一个复杂对象里的所有属性都变成“可选”的,偏偏普通的 Partial 只能做浅层次的——比如一个嵌套的 user 对象,Partial<User> 只能让 user.name 可选,没法自动让 user.address.city 也变成可选。这时候就得用到“深度 Partial”了,它就是把对象里每一层的属性都递归改成可选,像剥洋葱一样,从外到内每一层都松绑。
为啥普通 Partial 不够用?
举个生活化的例子:你订外卖填地址,外层的 Address 对象有省、市、区、街道,都是必填的。如果用普通 Partial<Address>,你最多能让“省”可选,但“市”“区”还是得填——可万一你只说“我在浙江杭州”,不想填具体区和街道呢?这时候普通 Partial 就卡壳了,它只改最外层,管不了内层,而深度 Partial 就是帮你把所有层级的必填都变成“可填可不填”。

手写深度 Partial,用递归解决嵌套问题
要做深度 Partial,核心是“递归”——遇到对象就把它的属性也转成深度可选,遇到基本类型就直接留着。我们先写个基础的类型定义:
`typescript
type DeepPartial<T> = {
// 遍历T的每一个属性键K
[K in keyof T]?:
// 判断属性T[K]是不是对象(排除null,因为typeof null也是object)
T[K] extends object
? DeepPartial<T[K]>
: T[K];
};
`
再用刚才的地址模型试试:
`typescript
interface Address {
province: string;
city: string;
detail: {
district: string;
street: string;
};
}
// 用DeepPartial处理后,所有属性都是可选的
const partialAddress: DeepPartial<Address> = {
province: "浙江",
city: "杭州",
// 不用填district、street,也符合类型要求
};
`
这里的递归就是关键:当遇到 detail 这个对象属性时,自动调用 DeepPartial 把它的 district 和 street 也变成可选,完美解决嵌套的问题。
避开两个常见的坑
实际用的时候,容易踩这两个小坑,得留意:
第一个坑:Array 类型别误判。比如地址里的 tags: string[],typeof [] 是 object,如果直接用上面的类型,会把数组的元素也递归转成 DeepPartial——比如 tags 会变成 Array<DeepPartial<string>>,其实我们只需要让数组本身可选,元素还是原来的类型就行。所以要加个判断,排除 Array:
`typescript
type DeepPartial<T> = {
[K in keyof T]?:
T[K] extends Array<any>
? T[K]
: T[K] extends object
? DeepPartial<T[K]>
: T[K];
};
`
这样 tags 还是 string[],不会乱改。
第二个坑:Date 等内置对象也别递归。如果属性是 Date 类型,typeof Date 是 object,但我们不需要把 Date 的属性转成可选,直接保留就行,所以再加个判断:
`typescript
type DeepPartial<T> = {
[K in keyof T]?:
T[K] extends Array<any>
? T[K]
: T[K] extends Function
? T[K]
: T[K] extends object
? DeepPartial<T[K]>
: T[K];
};
`
加完 Function 的判断,那些函数类型的属性也不会被误处理了。
现成工具类也能搞定
如果不想自己手写,很多常用的 TS 工具库(像 utility-types)已经封装好了 DeepPartial,直接导入用就行,适合快速开发的场景。但如果是做项目内部的基础类型定义,自己写一个适配业务需求的,会更灵活。
深度 Partial 本质就是“递归处理嵌套对象的每个层级”,把复杂对象的所有属性都变成可选,刚好对应了实际开发里那种“只传部分数据”的场景,不管是做表单、接口参数还是配置对象,用它都能少写很多冗余代码。