2

I'm trying to figure out how to provide a type definition that corresponds to "all classes that implement some abstract class." Take the following code example:

abstract class AbstractFoo {
  abstract foo()
}

class Concrete1 extends AbstractFoo {
  foo() { ... }
}

class Concrete1 extends AbstractFoo {
  foo() { ... }
}

Now, I'm trying to create a map that goes from string to one of the concrete classes. Note that I am not trying to map into instances of the concrete classes. See the following:

const myMap: Map<string, typeINeedHelpWith> = new Map()
myMap.set('concrete1string', Concrete1)
myMap.set('concrete2string', Concrete2)

const instantiatedConcrete1 = new myMap.get('concrete1string')(...)

Is there a type definition for typeINeedHelpWith that would let me accomplish this?

entersudonym
  • 196
  • 1
  • 7
  • It will be `const myMap: Map AbstractFoo>`, then `const instantiatedConcrete1 = new (myMap.get('concrete1string'))!()` – Aleksey L. Apr 11 '20 at 07:49

3 Answers3

2

Use a function that returns the concrete instance as map value.

Update: The below suggestion is just valid for deno

As a suggestion, it is better to use a Record type instead of a Map because otherwise wrong keys (mymap.get("concrete3")()) will be noticed as runtime exceptions.

abstract class AbstractFoo {
    abstract foo(): number;
  }

  class Concrete1 extends AbstractFoo {
    foo() {
      return 1;
    }
  }

  class Concrete2 extends AbstractFoo {
    foo() {
      return 2;
    }
  }

  const myMap: Record<string, () => AbstractFoo> = {
    "concrete1": () => new Concrete1(),
    "concrete2": () => new Concrete2(),
  };

  const instantiatedConcrete1 = myMap.concrete1();

  let fooResult = instantiatedConcrete1.foo();

As noted in the comments, notice that I've used a factory pattern for creating objects.

This is recognized as a good design pattern, but when not required a simpler constructor based solution may be more appropriate:

const myRec: Record<string, new() => AbstractFoo> = {
  "concrete1": Concrete1,
  "concrete2": Concrete2,
};

const iConcrete1 = new myRec.concrete1();

The sintax new() => AbstractFoo define the signature of a constructor that takes no arguments and returns object with shape AbstractFoo.

attdona
  • 17,196
  • 7
  • 49
  • 60
  • No need to wrap in factory function. Just `const myMap: Record AbstractFoo> = { "concrete1": Concrete1, "concrete2": Concrete2 };`, then `const instantiatedConcrete1 = new myMap.concrete1();` – Aleksey L. Apr 11 '20 at 12:27
  • Also "object map" (`Record<...>`) has exactly the same problem with wrong keys: `myMap.concrete3();`... – Aleksey L. Apr 11 '20 at 12:32
  • 1
    My wrong, I've verified my answer only with deno, Thanks for the feedback! – attdona Apr 11 '20 at 13:33
0

AFAIK the only type available is typeof AbstractFoo, that is the constructor type. Like typeof Array refers to the type of the Array constructor, while the type Array refers to the type of array instances.

Both Concrete1 and Concrete2 extend that type (more precisely typeof Concrete1 and typeof Concrete2 extend typeof AbstractFoo)

But you can't create a map like the following:

const myMap: Map<string, typeof AbstractFoo> = new Map()
myMap.set('concrete1string', Concrete1)
myMap.set('concrete2string', Concrete2)

Nay, you can, but the return type of myMap.get('concrete1string') is typeof AbstractFoo, not typeof Concrete1. You cannot call, with new, a constructor of type typeof AbstractFoo.

Andrea Simone Costa
  • 1,164
  • 6
  • 16
0

You can try something like this, using mapped types:

abstract class AbstractFoo {
    abstract foo(): string;
}

class Concrete1 extends AbstractFoo {
  foo() { return "Concrete 1" }
}

class Concrete2 extends AbstractFoo {
  foo() { return "Concrete 2" }
}

interface Mapping {
    'concrete1string': Concrete1,
    'concrete2string': Concrete2
}

class MyHeplerMap {
    maps: Mapping = {
        'concrete1string': new Concrete1(),
        'concrete2string': new Concrete2()
    };

    get<T extends keyof Mapping>(type: T): Mapping[T] {
        return this.maps[type];
    }
}

const map = new MyHeplerMap();
const c = map.get('concrete1string');
console.log(c.foo());

Please see playground link.

zhuber
  • 5,364
  • 3
  • 30
  • 63