如何基于递归对象生成带点路径的TypeScript判别联合类型?
动态生成嵌套回调的判别联合类型
我在TypeScript里定义了一个嵌套的回调函数对象:
type CallbackFn = (args: any) => any type CallbackObj = { [key: string]: CallbackFn | CallbackObj } const callbacks = { foo: function(args: { x: number }): string { return "test"; }, bar: { bar1: function(): boolean { return true; }, bar2: function(): number { return 10; } }, baz: { baz1: { baz2: function(args: { key: string }): string { return "test"; } } } }
还有这些接口定义:
interface FieldDef { name: string type: string callback: CallbackDef } interface CallbackDef { fn: string settings: any }
我的目标是:当用户声明FieldDef的回调时,能自动补全可选的回调路径(比如"foo"|"bar.bar1"|"bar.bar2"|"baz.baz1.baz2"),同时自动补全对应回调的settings参数类型。理想的CallbackDef应该是这样的判别联合类型:
type CallbackDef = { name: "foo", settings: { x: number } } | { name: "bar.bar1" } | { name: "bar.bar2" } | { name: "baz.baz1.baz2", settings: { key: string } }
但我没法基于已有的callbacks对象动态生成这个联合类型,现在卡在两个点:
- 递归处理多层嵌套的类型
- 常规索引类型没法处理对象节点下多个函数的情况
我目前的实现代码是:
type CallbackFn = (args: any) => any type CallbackObj = { [key: string]: CallbackFn | CallbackObj } const callbacks = { foo: function(args: { x: number }): string { return "test"; }, bar: { bar1: function(): boolean { return true; }, bar2: function(): number { return 10; } }, baz: { baz1: { baz2: function(args: { key: string }): string { return "test"; } } } } type StringKeys<T> = Extract<keyof T, string>; type Process<T> = { [key in StringKeys<T>]: T[key] extends CallbackFn ? { [k in key]: T[key] } : { [k in StringKeys<T[key]> as `${key}.${k}`]: T[key][k] } } type GetValues<T> = T[keyof T]; type A = Process<typeof callbacks> type B = GetValues<A>
求更简洁的实现方式。
解决方案
可以通过递归类型遍历嵌套结构,自动提取所有回调路径和对应的参数类型:
type CallbackFn = (args: any) => any type CallbackObj = { [key: string]: CallbackFn | CallbackObj } const callbacks = { foo: function(args: { x: number }): string { return "test"; }, bar: { bar1: function(): boolean { return true; }, bar2: function(): number { return 10; } }, baz: { baz1: { baz2: function(args: { key: string }): string { return "test"; } } } } // 提取对象的字符串键 type StringKeys<T> = Extract<keyof T, string>; // 递归提取所有回调路径 type ExtractCallbackPaths<T, Prefix extends string = ""> = { [K in StringKeys<T>]: T[K] extends CallbackFn ? Prefix extends "" ? K : `${Prefix}.${K}` : ExtractCallbackPaths<T[K], Prefix extends "" ? K : `${Prefix}.${K}`> }[StringKeys<T>]; // 根据路径解析对应的回调函数类型 type ParsePath<T, Path extends string> = Path extends `${infer K}.${infer Rest}` ? K extends StringKeys<T> ? T[K] extends CallbackObj ? ParsePath<T[K], Rest> : never : never : Path extends StringKeys<T> ? T[Path] extends CallbackFn ? T[Path] : never : never; // 获取回调函数的参数类型,无参数则返回never type CallbackArgs<T extends CallbackFn> = T extends (args: infer A) => any ? A : never; // 生成最终的CallbackDef联合类型 type CallbackDef = { [Path in ExtractCallbackPaths<typeof callbacks>]: { name: Path; settings: CallbackArgs<ParsePath<typeof callbacks, Path>> extends never ? undefined : CallbackArgs<ParsePath<typeof callbacks, Path>>; } }[ExtractCallbackPaths<typeof callbacks>]; // 测试:自动补全和类型提示 const field: FieldDef = { name: "test", type: "string", callback: { name: "baz.baz1.baz2", settings: { key: "hello" } // 自动提示key:string类型 } };
关键说明
ExtractCallbackPaths:递归遍历嵌套对象,拼接所有回调函数的完整路径,生成路径联合类型ParsePath:根据路径字符串反向查找对应的回调函数类型CallbackArgs:提取回调函数的参数类型,无参数时返回never,最终转为undefined(可根据需求调整为可选属性)- 最终生成的
CallbackDef是判别联合类型,选中不同name时,settings会自动匹配对应参数类型,实现自动补全
内容的提问来源于stack exchange,提问作者Owen Allen
相关产品推荐
相关产品推荐

