entersudonym
entersudonym

Reputation: 187

Typescript Type Declaration for Abstract Class Implementations

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?

Upvotes: 2

Views: 1235

Answers (3)

attdona
attdona

Reputation: 18943

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.

Upvotes: 2

zhuber
zhuber

Reputation: 5524

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.

Upvotes: 0

Andrea Simone Costa
Andrea Simone Costa

Reputation: 1264

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.

Upvotes: 0

Related Questions