admin管理员组

文章数量:1289911

I have a Base generic class:

abstract class BaseClass<T> {
  abstract itemArray: Array<T>;

  static getName(): string {
    throw new Error(`BaseClass - 'getName' was not overridden!`);
  }

  internalLogic() {}
}

and inheritors:

type Item1 = {
  name: string
}
class Child1 extends BaseClass<Item1> {
  itemArray: Array<Item1> = [];
  static getName(): string {
    return "Child1";
  }
}


type Item2 = {
  name: number
}
class Child2 extends BaseClass<Item2> {
  itemArray: Array<Item2> = [];
  static getName(): string {
    return "Child2";
  }
}

Now I want to have to define an object with the inheritors as its values:

type IChildrenObj = {
  [key: string]: InstanceType<typeof BaseClass>;
};

/* 
The following error is received: Type 'typeof BaseClass' does not satisfy the constraint 'new (...args: any) => any'.
  Cannot assign an abstract constructor type to a non-abstract constructor type. ts(2344)
*/

const Children: IChildrenObj = {
  C1: Child1,
  C2: Child2,
}

Lastly, I want to be able to use statics methods of the children, and also be able to create instances of them:

const child: typeof BaseClass = Children.C1;
/*
received the following error: Property 'prototype' is missing in type '{ getName: () => string; }' but required in type 'typeof BaseClass'. ts(2741)
*/

console.log(child.getName());
const childInstance: BaseClass = new child();
/*
The following 2 errors are received:
(1) Generic type 'BaseClass<T>' requires 1 type argument(s). ts(2314)
(2) Cannot create an instance of an abstract class. ts(2511)
Generic type 'BaseClass<T>' requires 1 type argument(s). ts(2314)
*/

I have a Base generic class:

abstract class BaseClass<T> {
  abstract itemArray: Array<T>;

  static getName(): string {
    throw new Error(`BaseClass - 'getName' was not overridden!`);
  }

  internalLogic() {}
}

and inheritors:

type Item1 = {
  name: string
}
class Child1 extends BaseClass<Item1> {
  itemArray: Array<Item1> = [];
  static getName(): string {
    return "Child1";
  }
}


type Item2 = {
  name: number
}
class Child2 extends BaseClass<Item2> {
  itemArray: Array<Item2> = [];
  static getName(): string {
    return "Child2";
  }
}

Now I want to have to define an object with the inheritors as its values:

type IChildrenObj = {
  [key: string]: InstanceType<typeof BaseClass>;
};

/* 
The following error is received: Type 'typeof BaseClass' does not satisfy the constraint 'new (...args: any) => any'.
  Cannot assign an abstract constructor type to a non-abstract constructor type. ts(2344)
*/

const Children: IChildrenObj = {
  C1: Child1,
  C2: Child2,
}

Lastly, I want to be able to use statics methods of the children, and also be able to create instances of them:

const child: typeof BaseClass = Children.C1;
/*
received the following error: Property 'prototype' is missing in type '{ getName: () => string; }' but required in type 'typeof BaseClass'. ts(2741)
*/

console.log(child.getName());
const childInstance: BaseClass = new child();
/*
The following 2 errors are received:
(1) Generic type 'BaseClass<T>' requires 1 type argument(s). ts(2314)
(2) Cannot create an instance of an abstract class. ts(2511)
Generic type 'BaseClass<T>' requires 1 type argument(s). ts(2314)
*/

Share Improve this question edited May 16, 2021 at 15:38 A-S asked May 16, 2021 at 15:26 A-SA-S 3,1533 gold badges31 silver badges44 bronze badges 17
  • 1 Please share Offer class/type – captain-yossarian from Ukraine Commented May 16, 2021 at 15:34
  • 1 Do you want values to be instances of BaseClass or classes? – captain-yossarian from Ukraine Commented May 16, 2021 at 15:40
  • 1 If you want class constructors to be values, then you don't want InstanceType. The type IChildrenObj is not going to be useful the way you want; instead it seems you just want to constrain Children to 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 annotating child1 with typeof BaseClass (inappropriate) or childInstance as BaseClass (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? – jcalz Commented May 16, 2021 at 16:48
  • 1 Maybe this article will be helpfull for you catchts./oop-style – captain-yossarian from Ukraine Commented May 16, 2021 at 17:27
  • 1 @A-S I think @captain-yossarian wrote that article, but credited me with some piece of code in it. Anyway, does this work for you? Essentially BaseClass<any> is as close to "some BaseClass<T> for some type T I 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 use BaseClass<any> and move on. If that works I can write up an answer. If not, let me know what issues remain. – jcalz Commented May 16, 2021 at 17:48
 |  Show 12 more ments

1 Answer 1

Reset to default 12

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 inpatible.
  C2: Child2, // error!
  // Type 'typeof Child2' is not assignable to type 'typeof BaseClass'.
  // Construct signature return types 'Child2' and 'BaseClass<T>' are inpatible.
}

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() // piles okay, but
} catch (err) {
  console.log(err); // 

本文标签: javascriptTypescript type of subclasses of an abstract generic classStack Overflow