You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何从扁平配置推断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" }
    }
  }
}

实现思路

  1. 约束扁平配置类型:使用泛型K捕获配置的键联合类型,同时限制parentKey只能是K的子集(确保父键存在)
  2. 递归构建树形类型:通过条件类型递归遍历配置,将每个节点的子节点嵌入到对应父节点的属性中
  3. 自动注入默认属性:在树形节点类型中合并默认属性,无需手动指定

代码实现

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 02:55:39