打字稿 - 正确输入具有绑定 运行 的函数映射

Typescript - Properly typing a map of functions that have bind run on them

我正在尝试设置类型,使我可以为映射中的一组函数完成代码完成,这些函数绑定了相同的变量,而无需手动声明描述输出的接口。

我可以用一个函数很好地做到这一点(参见下面的 performBind 打字),我可以获得打字来识别地图中的键并知道它是一个函数,但它总是有签名 (...args: any[]) => void


// Removes the first param from a tuple of params
type Tail<T extends any[]> =
  ((...args: T) => any) extends ((head: any, ...tail: infer R) => any) ? R : never;

// The input to the map function type
type ActionMapping<A, F extends (...args: any[]) => void>  = {
  [key in keyof A]: F
};

// The output from the map function type.
type PostActionMapping<A, F extends (...args: any[]) => void>  = {
  [key in keyof A]: (...args: Tail<Parameters<F>>) => ReturnType<F>
};

// Single binding function - basically what is being applied to each function in a map, but for a single function
function performBind<F extends (...args: any) => any>(func: F, binding: any): (...args: Tail<Parameters<F>>) => ReturnType<F> {
  return func.bind(null, binding);
}

// Mapping function
function associateActions<A, F extends (...args: any[]) => void>(actions: ActionMapping<A, F>, toBind: number): PostActionMapping<A, F> {
  const actionList: PostActionMapping<A, F> = {} as  PostActionMapping<A, F>  ;
  Object.keys(actions).forEach(key => {

    if (typeof actions[key as keyof A] === 'function') {

      actionList[key as keyof A] =  performBind(actions[key as keyof A], toBind);
    }
  });

  return actionList;
}

// Single function mapping test
const testing = performBind((numberArg: number, stringArg: string): string => {
  return `${numberArg} - ${stringArg}`;
}, 3);

testing('test'); // types as `testing(stringArg: string): void` - Correct!


// My action map
const myActions = {
  testing: (numberArg: number, stringArg: string): string => {
    return `${numberArg} - ${stringArg}`;
  },
};

// Testing w/ my map above
const mapped = associateActions(myActions, 3);
mapped.testing('test'); // types as `mapped.testing(...args: any[]): void` - Missing the typing

如前所述,从地图中出来的都是 (...args: any[]):void。一切 工作 ,但自动完成和输入正确当然会很棒。

我用于 associateActions() 的输入如下:

declare function associateActions<
  V,
  O extends Record<keyof O, (a0: V, ...args: any) => any>
>(
  actions: O,
  toBind: V
): {
  [K in keyof O]: O[K] extends (a0: V, ...args: infer A) => infer R
    ? (...args: A) => R
    : never
};

从该类型的值推断裸类型参数通常比从该类型的某个函数的值推断更容易。因此,我没有将 actions 设为 ActionMapping<A, F>,而是将其设为 O。是的,inference from mapped types is a thing, but it's still not as reliable as just inferring O from a value of type O. I've constrained the type of actions to be a mapping from keys to functions of at least one argument, where the first argument must be of type V, the same type as toBind. The return type maps O to a conditional 类型去掉了每个 属性 的第一个参数。它相当于 (...args: Tail<Parameters<O[K]>>) => ReturnType<O[K]>

你,呃,确实希望属性的 return 类型是原始函数的 return 类型,对吧?我不确定你为什么要使用 void,但你不必使用。

我将由您来实现 associateActions() 以进行类型检查(您可能只想使用单一调用签名 overload)。

但是让我们测试一下输入:

// My action map
const myActions = {
  testing: (numberArg: number, stringArg: string): string => {
    return `${numberArg} - ${stringArg}`;
  }
};

// Testing w/ my map above
const mapped = associateActions(myActions, 3);
// const mapped: (stringArg: string) => string
const str = mapped.testing("test"); 
// const str: string;

在我看来很合理...myActions.testing 需要一个 number 和一个 string 和 return 一个 string,所以 mapped.testing只需要一个 string 和 return 一个 string。好的,希望有帮助。祝你好运!

Link to code