打字稿仅获取 callable/executable 成员的类型

Typescript get type of only callable/executable members

我有一个包含混合成员类型、字段和方法的 class(例如下面的 MyClass)。 我正在尝试创建一个调用函数并能够在保持 this 上下文的同时执行它的命令。

如果我使用 class 其中没有字段,只有可调用成员(例如,如果我注释掉 public field: number = 1;),下面的代码可以正常工作,良好的类型感,错误如果 class 方法不存在,如果参数错误则出错。但是当我传递具有不可调用成员的 class 时,它有点不喜欢它。 也许你们有解决方案?

TS playground example

class MyClass{
  constructor() {}
  
  public field: number = 1;

  public printMessage(): void{
    console.log("message")
  }
  public printNumber(i: number): number{
    return i;
  }
}

type TypeOfClassMethod<T, M extends keyof T> = T[M] extends Function ? T[M] : never;

interface ICommand{

    execute(): any;
}

 class Command<F extends keyof MyClass> implements ICommand{
    api: MyClass;

    constructor(
        api: MyClass, 
        private func: TypeOfClassMethod<MyClass, F>, 
        private args:  Parameters<TypeOfClassMethod<MyClass, F>> //
        ) {
        this.api = api;
    }
    
    execute():  ReturnType<TypeOfClassMethod<MyClass, F>>{ 
 // error[1] type ‘TypeOfClassMethod<MyClass, F>’ does not satisfy the constraint ‘(...args: any) => any’.
 // Type ‘Function & MyClass[F]’ is not assignable to type ‘(...args: any) => any’.
        return this.func.call(this.api, ...this.args)
    }
}


let instance = new MyClass();

const command = new Command<"printNumber">(instance, instance.printNumber, [5] );
const wrongCOmmand = new Command<"field">(instance, instance.field, [] );

编辑: 我的实现的主要问题是如果我在 class 中有非函数成员,它会出错如果我想使用不可执行的成员创建命令,则会出错。

基本上我想在一个包装函数中调用它 callWithRetry 它会传递一个命令并执行它直到成功或满足特定的中断条件。

Take a look here, as an example (TS playground)

Or this one which is based on your first answer's second option

当我尝试在函数中调用它时,两者都缺少 return 类型。

function executeCommand < InstanceType > (command: Command < InstanceType > ) {
 // i want to use this in a manner similar to this, in a retry mechanism, but in this case I lose the returnType
  return command.execute()
}

const result = executeCommand(command); // result will have a return type of any

更新: 我让它按照我想要的方式工作,非常感谢你的帮助! here is the solution in TS playground

Explanation/description 在评论中

class MyClass {
  constructor() { }

  public field: number = 1;

  public printMessage(): void {
    console.log("message")
  }
  public printNumber(i: number): number {
    return i;
  }
}

// Base type for any function/method
type Fn = (...args: any) => any

// Obtain union of all values
type Values<T> = T[keyof T]

/**
 * Converts object to dictionary where
 *  keys are just object keys
 *  values are a tuples where 
 * 
 *    first element is key value
 *    second element is an array of arguments
 * 
 *      if first element is not a method,
 *      second element will be empty array
 */
type ObtainMethods<T> = {
  [Prop in keyof T]: T[Prop] extends Fn ? [T[Prop], Parameters<T[Prop]>] : [T[Prop], []]
}
{
  //   type Test = {
  //     field: [number, []];
  //     printMessage: [() => void, []];
  //     printNumber: [(i: number) => number, [i: number]];
  // }
  type Test = ObtainMethods<MyClass>
}

/**
 * Params obtains a union of all object values
 * Since we have a union of tuples, we can use it
 * for typing rest parameters
 */
type Params<T> = Values<ObtainMethods<T>>
{
  // | [[number, []], []] 
  // | [[() => void, []], []]
  // | [[(i: number) => number, [i: number]], []]
  type Test = Params<ObtainMethods<MyClass>>
}

interface ICommand {
  execute(): any;
}


class Command<Instance>{
  api: Instance;
  args: Params<Instance>

  constructor(
    api: Instance,
    ...args: Params<Instance>
  ) {
    this.api = api;
    this.args = args
  }

  execute() {
    if (this.args[0] instanceof Function) {
      return this.args[0].call(this.api, ...this.args)
    }
  }
}


let instance = new MyClass();

const command = new Command(instance, instance.printNumber, [5]); // ok
const command2 = new Command(instance, instance.printNumber, ['str']); // expected error

const wrongCOmmand1 = new Command(instance, instance.field, []); // ok
const wrongCOmmand2 = new Command(instance, instance.field, []); // ok

Playground