如何在TypeScript中正确定义递归分组函数类型?
问题:创建带绑定函数的类型安全值组工厂
我想定义一组函数类型,这组函数会根据参数生成包含这些函数(及其他值)引用的集合。下面是我的基础实现示例,但存在类型推断为any、getter隐式类型报错等问题。目标是让组内函数能通过组实例调用,只传递核心功能所需参数(已绑定组实例),同时移除getter并保证完全的类型安全。
type ValueGroup = Record<string, unknown> type ValueGroupCreationCallback = (...args: any[]) => ValueGroup export class ValueGroupFactory< CreationCallback extends ValueGroupCreationCallback > { creationCallback: CreationCallback constructor(creationCallback: CreationCallback) { this.creationCallback = creationCallback } createGroup(...params: Parameters<CreationCallback>) { return this.creationCallback.call( null, ...params ) as ReturnType<CreationCallback> } } function member1(g: ReturnType<typeof factory.createGroup>, string: string) { // Error return [string, g] } function member2(g: ReturnType<typeof factory.createGroup>, number: number) { // Error return [number, g] } const factory = new ValueGroupFactory((name: string) => { // Error const group = { person: { name }, get member1() { // Error: 'member1' implicitly has return type 'any' because it does not have a return type annotation... return member1.bind(null, group) }, get member2() { // Error: 'member2' implicitly has return type 'any' because it does not have a return type annotation... return member2.bind(null, group) } } return group }) const group1 = factory.createGroup("John") // Has type `any` instead of { person, member1, ... }
补充说明
这些组内函数原本是纯函数式的顶层函数,比如toggleVisibility用于切换元素可见性。现在需要支持用户自定义类前缀,且库可能存在多实例,所以要用工厂创建组。要求保留原有API,通过组实例g调用(如g.toggleVisibility()),函数内部调用其他组内函数也通过实例,且函数已绑定组实例,调用时只需传递核心功能相关参数。
非类型安全的示例模式:
function hasClass(g, element, classID) { // 检查元素是否包含指定类的逻辑,返回boolean } function addClass(g, element, classID) { // 添加类的逻辑 } function removeClass(g, element, classID) { // 移除类的逻辑 } function toggleVisibility(g, element) { if (g.hasClass(element, "visibility-toggle")) g.removeClass(element, "visibility-toggle") else g.addClass(element, "visibility-toggle") } // DangerAlert是一个组件,可传入不同配置 function DangerAlert(config: { classPrefix }) { // 使用工厂创建组实例 const g = dangerAlertGroupFactory.createGroup({ classPrefix: config.classPrefix }) const renderableElement = document.createElement("div") setInterval(() => g.toggleVisibility(renderableElement), 1000) }
解决方案
核心思路
- 用工具类型自动推导绑定组实例后的函数类型,避免手动重复定义
- 显式声明组的完整类型,让TypeScript能准确推断结构,杜绝
any - 移除getter,在创建组时直接生成绑定后的函数,解决隐式类型问题同时提升性能
完整类型安全实现
基础示例修复版
// 工具类型:自动绑定函数的第一个参数(组实例),返回新的函数类型 type BindFirstArg<Fn extends (g: any, ...rest: any[]) => any> = Fn extends (g: infer G, ...rest: infer Rest) => infer R ? (...rest: Rest) => R : never; // 原始组函数(第一个参数为组实例) function member1(g: Group, str: string) { return [str, g]; } function member2(g: Group, num: number) { return [num, g]; } // 显式定义组的完整类型:包含公共数据和绑定后的函数 type Group = { person: { name: string }; member1: BindFirstArg<typeof member1>; member2: BindFirstArg<typeof member2>; }; // 简化工厂类:直接推断创建回调的返回类型 export class ValueGroupFactory<GroupType> { private readonly creator: (...args: any[]) => GroupType; constructor(creator: (...args: any[]) => GroupType) { this.creator = creator; } createGroup(...params: Parameters<typeof this.creator>): GroupType { return this.creator(...params); } } // 创建工厂实例,直接在组初始化时绑定函数(移除getter) const factory = new ValueGroupFactory((name: string): Group => { // 先定义组的公共数据部分 const baseGroup = { person: { name }, }; // 绑定组实例到函数,生成最终的组 return { ...baseGroup, member1: member1.bind(null, baseGroup), member2: member2.bind(null, baseGroup), }; }); // 现在group1会正确推断为Group类型,不再是any const group1 = factory.createGroup("John"); // 调用时只需传核心参数,类型完全安全 group1.member1("test"); // 参数为string,符合要求 group1.member2(123); // 参数为number,符合要求
DOM操作场景适配版
针对补充说明中的类前缀需求,实现类型安全的组工厂:
// 复用工具类型 type BindFirstArg<Fn extends (g: any, ...rest: any[]) => any> = Fn extends (g: infer G, ...rest: infer Rest) => infer R ? (...rest: Rest) => R : never; // 定义组的公共配置和完整类型 type ClassGroupConfig = { classPrefix: string }; type ClassGroup = ClassGroupConfig & { hasClass: BindFirstArg<typeof hasClass>; addClass: BindFirstArg<typeof addClass>; removeClass: BindFirstArg<typeof removeClass>; toggleVisibility: BindFirstArg<typeof toggleVisibility>; }; // 原始函数,第一个参数为组实例 function hasClass(g: ClassGroup, element: Element, classID: string): boolean { return element.classList.contains(`${g.classPrefix}-${classID}`); } function addClass(g: ClassGroup, element: Element, classID: string): void { element.classList.add(`${g.classPrefix}-${classID}`); } function removeClass(g: ClassGroup, element: Element, classID: string): void { element.classList.remove(`${g.classPrefix}-${classID}`); } function toggleVisibility(g: ClassGroup, element: Element): void { const classID = "visibility-toggle"; if (g.hasClass(element, classID)) { g.removeClass(element, classID); } else { g.addClass(element, classID); } } // 针对类操作的专用工厂 export class ClassGroupFactory { createGroup(config: ClassGroupConfig): ClassGroup { // 绑定所有函数到组实例 return { ...config, hasClass: hasClass.bind(null, config), addClass: addClass.bind(null, config), removeClass: removeClass.bind(null, config), toggleVisibility: toggleVisibility.bind(null, config), }; } } // 使用示例 const dangerAlertGroupFactory = new ClassGroupFactory(); function DangerAlert(config: { classPrefix: string }) { const g = dangerAlertGroupFactory.createGroup(config); const renderableElement = document.createElement("div"); setInterval(() => g.toggleVisibility(renderableElement), 1000); }
方案优势
- 完全类型安全:组实例、函数参数、返回值的类型都能被TypeScript准确推断,错误会在编译阶段暴露
- 符合需求:函数调用时无需传递组实例,只需传入核心参数,内部调用其他组函数也通过实例
- 性能优化:移除getter,绑定操作只在组创建时执行一次,避免重复绑定的开销
内容的提问来源于stack exchange,提问作者Abdulramon jemil
相关产品推荐
相关产品推荐

