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

TypeScript中如何实现递归嵌套对象字面量的正确类型检查?

如何让嵌套的children属性根据tagName正确校验类型?

在TypeScript中,我们可以利用函数和泛型实现对象字面量的类型校验。比如示例中的createTestElement函数,它通过泛型TagNameT让attributes的可用属性键根据传入的tagName动态匹配——当tagName为'input'时,attributes可以设置type,但设置href会触发类型错误。

但这种方式在处理递归嵌套的children数组时失效了:children中每个元素的attributes类型被固定为所有标签属性的交集(只能使用className),无法根据自身的tagName匹配对应的属性,比如示例中子元素tagName: 'a'却无法设置href,会抛出类型错误。

示例代码:

interface ElementTypes {
  a: {
    href?: string;
    className?: string;
  };

  input: {
    type?: string;
    className?: string;
  };
}

type TestElement<TagNameT extends keyof ElementTypes = keyof ElementTypes> = {
  tagName: TagNameT;
  attributes: { [attr in keyof ElementTypes[TagNameT]]: any };
  children?: TestElement[];
};

function createTestElement<TagNameT extends keyof ElementTypes>(
  testElement: TestElement<TagNameT>,
): TestElement<TagNameT> {
  return testElement;
}

createTestElement({
  tagName: "a",
  attributes: { href: "http://example.com" }, // 正常
  children: [
    {
      tagName: "a",
      attributes: { href: "http://example.com" }, // 错误:类型'{ href: string; }'无法赋值给类型'{ className?: any; }'
    },
  ],
});

解决方案

问题出在TestElement类型的children定义上:当你写children?: TestElement[]时,这里的TestElement未指定泛型参数,会使用默认值keyof ElementTypes,导致attributes被推断为所有ElementTypes值类型的交集(仅className是共有的)。

要解决这个问题,我们需要让children中的每个元素可以是任意标签类型的TestElement,而非固定的交集类型。推荐两种实现方式:

方式一:使用联合类型明确任意标签元素

interface ElementTypes {
  a: {
    href?: string;
    className?: string;
  };

  input: {
    type?: string;
    className?: string;
  };
}

// 定义包含所有标签类型的TestElement联合类型
type AnyTestElement = {
  [Tag in keyof ElementTypes]: {
    tagName: Tag;
    attributes: ElementTypes[Tag];
    children?: AnyTestElement[];
  };
}[keyof ElementTypes];

// 适配任意标签类型的泛型函数
function createTestElement<T extends AnyTestElement>(testElement: T): T {
  return testElement;
}

// 测试代码:子元素attributes可正确匹配自身tagName
createTestElement({
  tagName: "a",
  attributes: { href: "http://example.com" }, // 正常
  children: [
    {
      tagName: "a",
      attributes: { href: "http://example.com" }, // 正常
    },
    {
      tagName: "input",
      attributes: { type: "text" }, // 正常
    },
  ],
});

方式二:直接修改原类型的children定义

interface ElementTypes {
  a: {
    href?: string;
    className?: string;
  };

  input: {
    type?: string;
    className?: string;
  };
}

// 修改children类型为任意标签的TestElement数组
type TestElement<TagNameT extends keyof ElementTypes = keyof ElementTypes> = {
  tagName: TagNameT;
  attributes: ElementTypes[TagNameT];
  children?: TestElement<keyof ElementTypes>[];
};

function createTestElement<TagNameT extends keyof ElementTypes>(
  testElement: TestElement<TagNameT>,
): TestElement<TagNameT> {
  return testElement;
}

// 测试代码:子元素attributes可正确匹配自身tagName
createTestElement({
  tagName: "a",
  attributes: { href: "http://example.com" }, // 正常
  children: [
    {
      tagName: "a",
      attributes: { href: "http://example.com" }, // 正常
    },
  ],
});

两种方式都能让children中的每个元素根据自身的tagName获得对应的attributes类型校验,解决了原有的类型交集问题。

内容的提问来源于stack exchange,提问作者Juan Iglesias

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 10:22:38