Firstly, the type
type IChildrenObj = {
[key: string]: InstanceType<typeof BaseClass>; // instances?
};
is not appropriate to describe your Children object. Children stores class constructors while InstanceType<typeof BaseClass>, even if it worked for abstract classes (which, as you noted, it doesn't), would be talking about class instances. It would be closer to write
type IChildrenObj = {
[key: string]: typeof BaseClass; // more like constructors
};
But that is also not what Children stores:
const Children: IChildrenObj = {
C1: Child1, // error!
// Type 'typeof Child1' is not assignable to type 'typeof BaseClass'.
// Construct signature return types 'Child1' and 'BaseClass<T>' are incompatible.
C2: Child2, // error!
// Type 'typeof Child2' is not assignable to type 'typeof BaseClass'.
// Construct signature return types 'Child2' and 'BaseClass<T>' are incompatible.
}
The type typeof BaseClass has an abstract construct signature that looks something like new <T>() => BaseClass<T>; the callers (or more usefully, the subclasses that extend BaseClass) can choose anything they want for T, and BaseClass must be able to handle that. But the types typeof Child1 and typeof Child2 are not able to produce BaseClass<T> for any T that the caller of new Child1() or the extender class Grandchild2 extends Child2 wants; Child1 can only construct a BaseClass<Item1> and Child2 can only construct a BaseClass<Item2>.
So currently IChildrenObj says it holds constructors that can each produce a BaseClass<T> for every possible type T. Really what you'd like is for IChildrenObj to say it holds constructors that can each produce a BaseClass<T> for some possible type T. That difference between "every" and "some" has to do with the difference between how the type parameter T is quantified; TypeScript (and most other languages with generics) only directly supports "every", or universal quantification. Unfortunately there is no direct support for "some", or existential quantification. See microsoft/TypeScript#14446 for the open feature request.
There are ways to accurately encode existential types in TypeScript, but these are probably a little too annoying to use unless you really care about type safety. (But I can elaborate if this is needed)
Instead, my suggestion here is probably to value productivity over full type safety and just use the intentionally loose any type to represent the T you don't care about.
So, here's one way to define IChildrenObj:
type SubclassOfBaseClass =
(new () => BaseClass<any>) & // a concrete constructor of BaseClass<any>
{ [K in keyof typeof BaseClass]: typeof BaseClass[K] } // the statics without the abstract ctor
/* type SubclassOfBaseClass = (new () => BaseClass<any>) & {
prototype: BaseClass<any>;
getName: () => string;
} */
type IChildrenObj = {
[key: string]: SubclassofBaseClass
}
The type SubclassOfBaseClass is the intersection of: a concrete construct signature that produces BaseClass<any> instances; and a mapped type which grabs all the static members from typeof BaseClass without also grabbing the offending abstract construct signature.
Let's make sure it works:
const Children: IChildrenObj = {
C1: Child1,
C2: Child2,
} // okay
const nums = Object.values(Children)
.map(ctor => new ctor().itemArray.length); // number[]
console.log(nums); // [0, 0]
const names = Object.values(Children)
.map(ctor => ctor.getName()) // string[]
console.log(names); // ["Child1", "Child2"]
Looks good.
The caveat here is that, while IChildrenObj will work, it's too fuzzy of a type to keep track of things you might care about, such as the particular key/value pairs of Children, and especially the weird "anything goes" behavior of index signatures and the any in BaseClass<any>:
// index signatures pretend every key exists:
try {
new Children.C4Explosives() // compiles okay, but
} catch (err) {
console.log(err); // 💥 RUNTIME: Children.C4Explosives is not a constructor
}
// BaseClass<any> means you no longer care about what T is:
new Children.C1().itemArray.push("Hey, this isn't an Item1") // no error anywhere
So my suggestion in cases like this is to only make sure that Children is assignable to IChildrenObj without actually annotating it as such. For example, you can use a helper function:
const asChildrenObj = <T extends IChildrenObj>(t: T) => t;
const Children = asChildrenObj({
C1: Child1,
C2: Child2,
}); // okay
Now Children can still be used anywhere you need an IChildrenObj, but it still remembers all of the specific key/value mappings, and thus emits errors when you do bad things:
new Children.C4Explosives() // compiler error!
//Property 'C4Explosives' does not exist on type '{ C1: typeof Child1; C2: typeof Child2; }'
new Children.C1().itemArray.push("Hey, this isn't an Item1") // compiler error!
// Argument of type 'string' is not assignable to parameter of type 'Item1'
You can still use IChildrenObj if you need to:
const anotherCopy: IChildrenObj = {};
(Object.keys(Children) as Array<keyof typeof Children>)
.forEach(k => anotherCopy[k] = Children[k]);
Playground link to code
InstanceType. The typeIChildrenObjis not going to be useful the way you want; instead it seems you just want to constrainChildrento something that has subclass constructors in it, in which case you'd be better off with a constrained helper function like this. The same with annotatingchild1withtypeof BaseClass(inappropriate) orchildInstanceasBaseClass(not valid type). Can you elaborate on why you need these types at all? Why not just use type inference as in the linked code?BaseClass<any>is as close to "someBaseClass<T>for some typeTI don't know and don't want to specify" as you can easily get. Maybe if there's ever some support added for existentially quantified generics you can do better, but for now I'd say just useBaseClass<any>and move on. If that works I can write up an answer. If not, let me know what issues remain.