TypeScript:如何定义含保留键与任意自定义键的递归对象类型
问题背景
需要定义一个包含两类保留属性的类型:
- 必填的
key(类型为string | number) - 可选的递归
children(类型为自身数组)
其余任意自定义键的类型必须是ReactNode | ((props: IRenderDataProps) => JSX.Element)
最初尝试的接口写法直接触发类型错误:
interface IDataProps { [key: string]: ReactNode | ((props: IRenderDataProps) => JSX.Element) // 除以下保留键外的任意键 key: string | number; // 保留键 children?: IDataProps[] // 保留键 }
错误信息:TS2411: Property 'children' of type 'IDataProps[]' is not assignable to 'string' index type 'ReactNode | ((props: IRenderDataProps) => JSX.Element)'
改用交叉类型后,浅层结构可正常使用,但嵌套层级达到2层及以上时再次报错:
type TDataSource = { [key: string]: ReactNode | ((props: IRenderDataProps) => JSX.Element); // 除以下保留键外的任意键 } & { key: string | number; // 保留键 children?: TDataSource[]; // 保留键 };
错误信息:TS2322: Type '{ key: number; name: () => JSX.Element; children: { key: number; name: () => JSX.Element; }[]; }' is not assignable to type 'TDataSource'...Property 'children' is incompatible with index signature.
错误原因
TypeScript的索引签名要求所有属性类型必须与索引签名类型兼容。不管是直接接口声明还是交叉类型,children的TDataSource[]类型都无法匹配索引签名定义的ReactNode | ((props: IRenderDataProps) => JSX.Element),嵌套场景下这个类型冲突会被直接触发。
正确解决方案
通过映射类型+排除保留键的组合方式,结合显式声明保留属性,可完美解决该问题:
import type { ReactNode, JSX } from 'react'; // 渲染props类型,根据业务需求补充字段 interface IRenderDataProps { // 自定义渲染参数 } type TDataSource = { key: string | number; children?: TDataSource[]; } & { [K in Exclude<string, 'key' | 'children'>]: ReactNode | ((props: IRenderDataProps) => JSX.Element); };
原理说明
- 显式声明
key和children两个保留属性,它们的类型不受后续映射类型约束 - 使用
Exclude<string, 'key' | 'children'>确保映射类型仅作用于除保留键外的所有字符串键 - 映射类型强制要求所有非保留键的类型必须为
ReactNode或返回JSX元素的函数
验证示例
嵌套结构可正常通过类型校验:
const nestingExample: TDataSource[] = [ { key: 5, [`custom-key-demo`]: () => <>SomeJSX</>, children: [ { key: 6, anotherCustomKey: '这是ReactNode类型的字符串' } ], }, ];
该方案既满足保留键的类型要求,也约束了自定义键的类型,同时支持无限层级的嵌套结构,不会触发类型错误。
内容的提问来源于stack exchange,提问作者Marty

