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
相关产品推荐
相关产品推荐

