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

如何在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)
}

解决方案

核心思路

  1. 用工具类型自动推导绑定组实例后的函数类型,避免手动重复定义
  2. 显式声明组的完整类型,让TypeScript能准确推断结构,杜绝any
  3. 移除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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 17:43:14