如何从扁平配置推断TypeScript嵌套递归类型?
扁平配置转树形结构的TypeScript类型标注方案
需求说明
需要实现一个函数,将扁平键值对配置转换为嵌套树形结构,同时满足以下类型约束:
- 捕获具体键名,拒绝模糊的字符串索引类型
- 严格限制
parentKey只能是配置中已存在的键 - 无需手动重复指定类型,自动为每个树形节点嵌入默认属性/行为
示例输入(扁平配置):
const flatConfig = { key1: { property: "value1" }, key2: { property: "value2" }, key3: { property: "value3", parentKey: "key2"}, key4: { property: "value4", parentKey: "key3" } }
示例输出(嵌套树形结构):
{ key1: { property: "value1", defaultProp: "defaultValue" }, key2: { property: "value2", defaultProp: "defaultValue", key3: { property: "value3", defaultProp: "defaultValue", key4: { property: "value4", defaultProp: "defaultValue" } } } }
实现思路
- 约束扁平配置类型:使用泛型
K捕获配置的键联合类型,同时限制parentKey只能是K的子集(确保父键存在) - 递归构建树形类型:通过条件类型递归遍历配置,将每个节点的子节点嵌入到对应父节点的属性中
- 自动注入默认属性:在树形节点类型中合并默认属性,无需手动指定
代码实现
1. 基础类型定义
// 节点自定义属性类型 type NodeProperties = { property: string; // 可扩展其他自定义属性 }; // 默认注入的节点属性/行为 type DefaultNodeProps = { defaultProp: string; // 可添加默认方法,比如: // doSomething: () => void; }; // 扁平配置类型:K为键的联合类型,每个节点的parentKey只能是K中的键或undefined type FlatConfig<K extends string> = Record<K, NodeProperties & { parentKey?: K }>;
2. 树形结构类型推导
// 提取某个父节点下的所有子节点键 type ChildKeys<Config extends FlatConfig<string>, ParentKey extends string> = { [K in keyof Config]: Config[K]['parentKey'] extends ParentKey ? K : never; }[keyof Config]; // 递归构建树形节点类型 type TreeNode<Config extends FlatConfig<string>, NodeKey extends string> = Omit<Config[NodeKey], 'parentKey'> & DefaultNodeProps & { [K in ChildKeys<Config, NodeKey>]?: TreeNode<Config, K>; }; // 最终树形结构类型:提取所有根节点(无parentKey或parentKey不存在的节点) type TreeStructure<Config extends FlatConfig<string>> = { [K in keyof Config as Config[K]['parentKey'] extends keyof Config ? never : K]: TreeNode<Config, K>; };
3. 转换函数实现
function flattenToTree<Config extends FlatConfig<string>>( config: Config, defaultProps: DefaultNodeProps = { defaultProp: "defaultValue" } ): TreeStructure<Config> { const tree: Partial<TreeStructure<Config>> = {}; const nodeMap = new Map<keyof Config, TreeNode<Config, keyof Config>>(); // 初始化所有节点,注入默认属性 Object.keys(config).forEach(key => { const node = { ...config[key], ...defaultProps }; delete (node as any).parentKey; nodeMap.set(key as keyof Config, node as TreeNode<Config, keyof Config>); }); // 构建树形结构 Object.keys(config).forEach(key => { const parentKey = config[key].parentKey; if (parentKey && nodeMap.has(parentKey)) { const parentNode = nodeMap.get(parentKey)!; parentNode[key as keyof typeof parentNode] = nodeMap.get(key)!; } else { tree[key as keyof TreeStructure<Config>] = nodeMap.get(key)!; } }); return tree as TreeStructure<Config>; }
4. 使用示例
const flatConfig = { key1: { property: "value1" }, key2: { property: "value2" }, key3: { property: "value3", parentKey: "key2"}, key4: { property: "value4", parentKey: "key3" } } as const; // 用as const让TypeScript捕获具体键值 const tree = flattenToTree(flatConfig); // 类型推断正确: // tree.key1 类型为 { property: "value1"; defaultProp: string; } // tree.key2.key3 类型为 { property: "value3"; defaultProp: string; key4?: ... }
优化建议
- 配置结构优化:可以给
parentKey添加更严格的约束(比如通过条件类型确保parentKey对应的键存在),避免无效父键:type ValidFlatConfig<K extends string> = { [Key in K]: NodeProperties & { parentKey?: Extract<K, Key> extends never ? K : never; // 避免自身作为父键 }; }; - 默认属性灵活注入:允许通过泛型参数自定义默认属性,增强复用性:
function flattenToTree<Config extends FlatConfig<string>, D extends object>( config: Config, defaultProps: D ): TreeStructure<Config, D> { /* ... */ }
内容的提问来源于stack exchange,提问作者Vadym S. Khondar
相关产品推荐
相关产品推荐

