Typescript get type of only callable/executable members

Viewed 252

I have a class with mixed member types, fields and methods (ex. MyClass below). I am trying to have a command created that calls a function and is able to execute it while keeping this context.

The code below works if I use a class that has no fields in it, only callable members ( ex. if i comment out public field: number = 1;) This works fine, good type sense, errors if class method does not exist, errors if bad params. but it kind of does not like it when i pass a class that has non-callable members. Maybe you guys have a solution for that?

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, [] );

EDIT: My main problem with the implementation i had is that it gives errors if i have non-function members in a class, I want it to work for other classes that have non-executable members but give me an error if I want to create a command with a member that is not executable.

Basically I want to call this in a wrapper function callWithRetry which gets passed a command and it executes it until it succeeds or a certain break criteria are met.

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

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

Both lack return type when I try to call it inside a function.

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

UPDATE: I got it to work how I wanted it, thank you so much for the help! here is the solution in TS playground

1 Answers

Explanation/description is in comments

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

Related