static factory within an abstract class in typescript

Viewed 127

Consider the following:

export type JSONable = Record<string, string | number>

export type Processable = {
  process(args?: JSONable): Promise<void>
}

abstract class AbstractTaskProcessor<T extends JSONable> implements Processable {

  abstract process(args?: T): Promise<void>

}

this is part of my job system. But sometimes for testing I want to test the process method right away, so I wanted to add a static helper method like so

abstract class AbstractTaskProcessor<T extends JSONable> implements Processable {

  abstract process(args?: T): Promise<void>

  static runNow(args?: T): Promise<void> {
    const instance = new this()
    return instance.process(args)
  }
}

but i get a TS2511: Cannot create an instance of an abstract class.

I would like the DSL to be something like ...

MyTaskProcessor.runNow({accountId: 123123, accountName: 'Lorum' })

Does anyone have a good pattern for this?


I am on TS 4.2

2 Answers

You could make your example work if you just typecast how @ccarton showed. But if it's only for tests and you want to preserve the type information of T extends JSONable, I would suggest writing a function for it like this:

class MyTaskProcessor extends  AbstractTaskProcessor<{test: number, test2: string}> {
     process(args?: {test: number, test2: string}): Promise<void> {
         return Promise.resolve();
     }
}

type Processable<T> = {
  process(args?: T): Promise<void>
}

export function runNow<T extends JSONable>(processorClass: new () => Processable<T>, args: T) {
  const instance = new processorClass();
  return instance.process(args)
}

runNow(MyTaskProcessor, {test: 5, test2: "hello"})
runNow(MyTaskProcessor, {test: 5, test2: 3}) // Type 'number' is not assignable to type 'string'
MyTaskProcessor.runNow({test: 5, test2: 3}) // no typesafety, no warning

Updated with a refined version from @Linda Paiste

Abstract constructor signatures are now supported as of Typescript 4.2. See: https://devblogs.microsoft.com/typescript/announcing-typescript-4-2/#abstract-construct-signatures

If for some reason you aren't able to upgrade, the old solution requires a regular constructor signature and a type assertion:

type JSONable = Record<string, string | number>

type Processable = {
  process(args?: JSONable): Promise<void>
}

type ProcessableConstructor = { new (): Processable }  // Constructor signature

abstract class AbstractTaskProcessor<T extends JSONable> implements Processable {

  abstract process(args?: T): Promise<void>

  static runNow<T extends JSONable> (args?: T): Promise<void> {
    const instance = new (this as any as ProcessableConstructor)()   // Type assertion
    return instance.process(args)
  }
}

class MyTaskProcessor extends AbstractTaskProcessor<JSONable> {
  async process (args: JSONable) { return }
}

MyTaskProcessor.runNow({accountId: 123123, accountName: 'Lorum' })
Related