1

I wanted to create a generic type that will hold a method that exists on a clas with parameters of this method, but when I am providing the class instance to the generic I get [never, never].

I would use it this type inside a class so I provide a simplified example of potential usage

Example usage inside a class

type AnyClass = { new (...arg0: any): any }

type SingeTask<V extends InstanceType<AnyClass>> = [
  Extract<keyof V, (...arg0: any) => any>,
  Parameters<Extract<keyof V, (...arg0: any) => any>>
]

type Task<V extends InstanceType<AnyClass>> = {
  task: SingeTask<V>
  resolve: (value?: unknown) => void
  reject: (reason: any) => void
}

class Queue<T extends AnyClass> {
  instances: InstanceType<T>[] = []
  queue: Task<T>[] = []
  object: T

  constructor(object: T) {
    this.object = object
  }

  addInstance( number:number) {
      for (i; i < number; i++) {
        this.instances.push(new this.object())
  }

  

  runTasksInQueue(
    instance: InstanceType<T>,
    { task, reject, resolve }: Task<T>
  ) {
    try {
      const respone = Reflect.apply(instance, ...task)
      resolve(respone)
    } catch (error) {
      reject(error)
    } finally {
      const task = this.queue.pop()
      if (task) {
        this.runTasksInQueue(instance, task)
      } else {
        this.instances.push(instance)
      }
    }
  }

  addTaskToQueue(task: SingeTask<T>) {
    return new Promise((resolve, reject) => {
      const instance = this.instances.pop()
      if (instance) {
        this.runTasksInQueue(instance, { task, resolve, reject })
      } else {
        this.queue.push({ task, resolve, reject })
      }
    })
  }
}

Usage of the class Queue

class mockClass {
  sth?: number
  constructor(n?: number) {
    this.sth = n
  }
  getNumber(n: number) {
    return n
  }
}

const instanceQueue = new queue(mockClass)
instanceQueue.addInstance(5)
instanceQueue.addTaskToQueue() // Here I get error [never,never] but I would expect to be able to pass [getNumber,(number e.g - 10)] and receive 10
Damian Grzanka
  • 275
  • 2
  • 13

1 Answers1

1

While your code is mostly correct in intent it has two issues to begin with:

  1. V extends InstanceType<AnyClass> does not mean V will be the instance type of whatever is passed in, it just means V must extend whatever is the instance type of AnyClass is, which is any. So this reduces to V extends any. So when you get SingeTask<typeof mockClass> in your code, V will be typeof mockClass not the instance type of mockClass. You need to remove the constraint and use InstanceType<V> anywhere you need the instance type

  2. Extract<keyof V, (...arg0: any) => any> will not filter the keys of V to only those that have a specific value. Extract will take out of a union, some the types that extend the second parameter. So for example Extract<"a" | "b", "a"> will be "a". Or more usefully Extract<Circle | Rectangle, { type: "circle" }> will be circle (Playground Link). To get keys by a specific type you need something like the type KeyOfType you see in this answer (I will just use KeyOfType, you can read the expiations in the referenced answer as to how it works)

Putting these two observation together we get the first usable iteration of SingleTask:

type KeyOfType<T, U> = {[P in keyof T]-?: T[P] extends U ? P: never}[keyof T]
type ValuesOfType<T, U> = {[P in keyof T]-?: T[P] extends U ? T[P]: never}[keyof T]
// Alternate version of ValuesOfType
// type ValuesOfType<T, U> = Extract<T[keyof T], U>

type SingeTask<V extends AnyClass> = [
  KeyOfType<InstanceType<V>, (...arg0: any) => any>,
  Parameters<ValuesOfType<InstanceType<V>, (...arg0: any) => any>>
]

Playground Link

Testing it out looks good at first :

class mockClass {
  public field: string = ""
  getNumber(n: number) { return n }
  getString(s: string) { return s }
}

const instanceQueue = new Queue(mockClass)
instanceQueue.addInstance(5)
instanceQueue.addTaskToQueue(["getNumber", [1]]) // one number is ok ✅
instanceQueue.addTaskToQueue(["getString", ["A"]]) // one string is ok ✅
instanceQueue.addTaskToQueue(["getString", ["A", "B"]]) // not ok ✅
instanceQueue.addTaskToQueue(["field", []]) // no fields ✅

But then we find a subtle issue, this is valid:

instanceQueue.addTaskToQueue(["getString", [1]]) // Allowed 

The issue is that there is no relation between the first member of the tuple, and the second. Applied to mock class SingeTask results in the type ["getNumber" | "getString", [number] | [string]]. While this does provide some safety, it does mean we ca mix the parameters of getNumber with the name of getString.

The solution to this is to generate a union of tuples instead of a tuple with union members as above. Basically we want ["getNumber", [number]] | ["getString", [string]].

We can apply the same ideas as KeyOfType and get the union of tuples containing key names and parameters instead of just key names:

type SingeTask<V extends AnyClass> = {
  [P in keyof InstanceType<V>]-?: InstanceType<V>[P] extends (...a: any) => any ? [P, Parameters<InstanceType<V>[P]>]: never
}[keyof InstanceType<V>]

Playground Link

With this new version we can't mix parameter of one function with the name of another function

instanceQueue.addTaskToQueue(["getString", [1]]) // Error ✅
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357