如何在TypeScript中选取嵌套字段并保留可选性与可空性?
解决TypeScript嵌套Pick保留字段可选性与可空性的问题
问题背景
我遇到的问题和嵌套Pick类型的需求类似,但更复杂——需要保留字段的null值与可选性。场景如下:
- 有一个配送页面的GraphQL查询,以及GraphQL端点生成的全量接口文件(比如
GQLCountry) - 不能直接使用完整接口,因为其中包含未查询的字段
- 需要基于生成的类型得到指定的
ShippingDataResponse接口 - 现有
Pick工具类型的方案会将address和country重新声明为非可选、非可空字段,导致类型错误 - 手动修改4000行的生成文件不现实,且文件可能随时变更,需要让类型始终与GraphQL输出保持同步
解决方案
我们可以自定义递归的工具类型,既能嵌套选择指定字段,又能完整保留原类型的可选性、可空性等特性。
自定义递归Pick工具类型
type DeepPick<T, K extends string> = K extends `${infer Key}.${infer Rest}` ? { [P in Key]: T[P] extends infer U ? U extends null | undefined ? U | (U extends object ? DeepPick<NonNullable<U>, Rest> : never) : U extends object ? DeepPick<U, Rest> : never : never } : { [P in K]: T[P] }; // 组合多个DeepPick结果的工具类型 type DeepPickMultiple<T, K extends string[]> = K extends [infer First, ...infer Rest] ? First extends string ? Rest extends string[] ? DeepPick<T, First> & DeepPickMultiple<T, Rest> : never : never : {};
用法示例
假设全量GraphQL生成类型如下:
interface GQLUser { id: string; name?: string | null; address?: { street: string; city?: string | null; country?: { code: string; name?: string | null; } | null; } | null; }
要生成只包含id、name、address.city、address.country.code的ShippingDataResponse:
type ShippingDataResponse = DeepPickMultiple<GQLUser, [ 'id', 'name', 'address.city', 'address.country.code' ]>;
生成的类型会完整保留原字段特性:
name仍为string | null | undefinedaddress仍为{ city?: string | null; country?: { code: string } | null } | null | undefinedaddress.city保持原有的string | null | undefinedaddress.country.code为必填string(继承原类型的必填属性)
关键特性说明
- 递归解析路径:通过模板字符串类型拆分嵌套字段路径(如
address.city拆分为address和city) - 保留可空性:判断原字段是否为
null或undefined,生成新类型时完整保留这些特性 - 保留可选性:通过映射类型
[P in Key]自动继承原字段的可选标记(?) - 支持多字段选择:
DeepPickMultiple可组合多个嵌套路径的选择结果
额外优化
如果需要支持嵌套排除字段(类似Omit的嵌套版本),可基于同样思路扩展:
type DeepOmit<T, K extends string> = K extends `${infer Key}.${infer Rest}` ? { [P in keyof T]: P extends Key ? T[P] extends infer U ? U extends null | undefined ? U | (U extends object ? DeepOmit<NonNullable<U>, Rest> : never) : U extends object ? DeepOmit<U, Rest> : never : never : T[P] } : Omit<T, K>;
内容的提问来源于stack exchange,提问作者BernaMariano
相关产品推荐
相关产品推荐

