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?

1
  • It will be const myMap: Map<string, new () => AbstractFoo>, then const instantiatedConcrete1 = new (myMap.get('concrete1string'))!() Commented Apr 11, 2020 at 7:49

4 Answers 4

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.

Sign up to request clarification or add additional context in comments.

3 Comments

No need to wrap in factory function. Just const myMap: Record<string, new () => AbstractFoo> = { "concrete1": Concrete1, "concrete2": Concrete2 };, then const instantiatedConcrete1 = new myMap.concrete1();
Also "object map" (Record<...>) has exactly the same problem with wrong keys: myMap.concrete3();...
My wrong, I've verified my answer only with deno, Thanks for the feedback!
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.

Comments

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.

Comments

0

I think i have figured something out:

type AnyInstanceOfAbstractClass<ClassType extends abstract new (...args: any[]) => any> =
  Omit<ClassType, 'constructor'>
  & (new (...args: ConstructorParameters<ClassType>) => InstanceType<ClassType>);

The first part is to get all the static properties of the class, but omits the abstract constructor that produces the error.

The second part defines a non-abstract constructor with the same signature as the abstract one.

To use this type, replace typeINeedHelpWith with AnyInstanceOfAbstractClass<typeof AbstractFoo>.

Your original example should now look like this:

abstract class AbstractFoo {
  abstract foo()
}

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

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

type AnyInstanceOfAbstractClass<ClassType extends abstract new (...args: any[]) => any> = Omit<ClassType, 'constructor'> &
  (new (...args: ConstructorParameters<ClassType>) => InstanceType<ClassType>);

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

const instantiatedConcrete1 = new (myMap.get('concrete1string')!)()
// slightly adapted above line to tell TypeScript that the return value will not be 'undefined'

Or see this example (slightly extended by a static property) in the TypeScript playground

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.