如何从不同类型的树形结构中递归提取公共字段?
如何从不同类型的树形结构中递归提取公共字段?
我完全懂你的困扰——你想从这个由Zod定义的多变体树形结构里,递归提取所有节点的id和children字段,方便写ID唯一性检查的函数和测试,但普通的Pick只能处理顶层类型,子节点还是带着原来的所有专属必填字段,导致手动创建的简化节点类型报错。别急,咱们写个递归版的Pick工具类型就能解决这个问题!
问题根源拆解
你现在用Pick<LeadingItem, 'id' | 'children'>,只会把顶层节点的类型精简成只含id和children,但children的类型还是原来的联合类型(followingFoo/followingBar/followingBaz的推断类型),这些子类型都要求必须有各自的专属字段(比如followingBazField),所以你手动创建的只带id和children的子节点就会触发报错——因为缺少那些必填的专属字段。
解决方案:递归Pick工具类型
我们可以定义一个RecursivePick类型,它会递归地对嵌套的children数组也应用Pick操作,把所有层级的节点都精简成你需要的结构:
type RecursivePick<T, K extends keyof T> = T extends unknown ? { [P in K]: P extends 'children' ? RecursivePick<T[P][number], K>[] : T[P] } : never;
这个类型的核心逻辑:
- 利用
T extends unknown触发联合类型的自动分发,确保每个变体类型都被单独处理 - 只保留你指定的键
K - 如果当前处理的键是
children,就对数组里的每一项递归应用RecursivePick,再重新组成数组 - 其他键直接保留原类型
结合你的代码实战
现在把这个工具类型用到你的代码里,就能生成符合预期的ItemWithIDAndChildrenOnly:
import { z } from 'zod' const followingFooSchema = z.object({ id: z.string(), get children() { return z.array(followingFooSchema) }, followingFooField: z.string() }); const followingBarSchema = z.object({ id: z.string(), get children() { return z.array(z.union([followingFooSchema, followingBarSchema])) }, followingBarField: z.boolean() }); const followingBazSchema = z.object({ id: z.string(), get children() { return z.array(z.union([followingFooSchema, followingBarSchema, followingBazSchema])) }, followingBazField: z.number() }); const leadingFooSchema = z.object({ id: z.string(), children: z.array(followingFooSchema), leadingFooField: z.string() }); const leadingBarSchema = z.object({ id: z.string(), children: z.array(z.union([followingFooSchema, followingBarSchema, followingBazSchema])), leadingBarField: z.boolean() }); const leadingItemSchema = z.union([leadingFooSchema, leadingBarSchema]); type LeadingItem = z.infer<typeof leadingItemSchema>; // 使用递归Pick工具类型生成目标类型 type ItemWithIDAndChildrenOnly = RecursivePick<LeadingItem, 'id' | 'children'>; // 现在这个赋值完全不会报错了! const item: ItemWithIDAndChildrenOnly = { id: "", children: [{ id: "", children: [] }] };
效果验证
现在ItemWithIDAndChildrenOnly会递归地把所有层级的节点都统一成{ id: string; children: ItemWithIDAndChildrenOnly[] }结构,不管原来的节点是Foo、Bar还是Baz类型,完全符合你想要的递归接口,写ID唯一性检查的函数和测试就轻松多了。
内容来源于stack exchange
相关产品推荐
相关产品推荐

