1

In TypeScript, it is possible to define a type of a constructable class using new(...args: any):

// We have some class inheritance
abstract class Something {}
class ChildOfSomething extends Something {}
class AnotherChildOfSomething extends Something {}

// This variable has a type that lets you assign any constructor of a child class of an abstract class
let constructorOfSomething: new (...args: ConstructorParameters<typeof Something>) => Something;

// We can assign different child classes dynamically and construct them
constructorOfSomething = ChildOfSomething;
let instanceOfSomething = new constructorOfSomething();
constructorOfSomething = AnotherChildOfSomething;
let anotherInstanceeOfSomething = new constructorOfSomething();

Since this is a useful but somewhat pattern, I wanted to make it a generic, but I get the following issue:

type ChildClassOf<BaseClass> = new (...args: ConstructorParameters<typeof BaseClass>) => BaseClass;
//------------------------------------------------------------------------^^^^^^^^^
//'BaseClass' only refers to a type, but is being used as a value here.ts(2693)

Is it possible to define this generic somehow?

5
  • Is there a usecase you want to solve with this? There might be ways to solve this problem using Templates / Generics, but having an interface for just allocating anything on the heap with little control over when (or if) it gets cleaned up again doesn't sound like a great idea. I am asking because it sounds like this question is treating symptoms of a problem we (as readers) don't know yet Commented Oct 9 at 10:55
  • 1
    I don't understand what memory management has to do with this - it's a GC language, the JS garbage collector does all the cleanup as needed. The usecase is directly in the question: I re-use this pattern in several places, and want a generic type instead of needing to type new (...args: ConstructorParameters<typeof AbstractClass>) => AbstractClass every time. Commented Oct 9 at 11:06
  • 2
    You can't do this using the instance type; an instance type doesn't know its constructor type. That the names are the same for class statements is a red herring; you can't generalize from it. Types and values live in separate namespaces. See this q/a. The closest to this is to pass the type of the constructor, not an instance (because constructor types do know their instance types), as shown in this playground link. Does that fully address the question? If so I'll write an answer or find a duplicate. If not, what's missing? Commented Oct 9 at 12:36
  • @jcalz I think this works perfectly. Thank you for explaining the finer differences, I wasn't aware of the specifics of the type system to that degree. If you want to put the playground code in an answer, will mark it as correct. Commented Oct 14 at 15:22
  • I'll do so when I get a chance, probably in five or six hours Commented Oct 14 at 15:39

1 Answer 1

1

You're making the same kind of category mistake as in Generic type to get enum keys as union string in typescript?. The typeof type operator acts on values, not types. If you have const v: V, you can write typeof v but you cannot write typeof V. Notice that types and values live in different namespaces, so you can have const X = {a: "abc"}; type X = {b: number}; and write typeof X, but you'll be getting the type of the const (like {a: string}) and not the type of the type. So if there happens to be a type with the same name as a value, you still can't access it with typeof.

For a class declaration like class Foo {}, there will be both a value and a type named Foo. The value is the class constructor (e.g., new Foo()) while the type is the type of a class instance (e.g., const f: Foo = ). But you can't use the name of the type to get to the type of the value. If you have a generic type like type ChildClassOf<T> = you can't pass in Foo as T and then write something like typeof T to get the type of the value named Foo. There is no typeof T if T is just a value.

Whatever you pass to ChildClassOf needs to be a type, and it needs to know enough about the constructor type to determine both the constructor parameters and the instance type. That can't be the instance type because a class instance type doesn't know anything about its constructor type. While at runtime you can use (new Foo()).constructor to recover Foo, the type of the constructor property in TypeScript is just Function. See microsoft/TypeScript#3841 for why this is.

Instead, you'll have to pass in the constructor type. The constructor type knows about the instance type and the constructor parameter list. That means you can write:

type ChildClassOf<BaseClass extends abstract new (...args: any) => any> =
    new (...args: ConstructorParameters<BaseClass>) => InstanceType<BaseClass>;

using both the ConstructorParameters and InstanceType utility types. You could also write

type ChildClassOf<BaseClass extends abstract new (...args: any) => any> =
    BaseClass extends abstract new (...args: infer A) => infer I ? new (...args: A) => I : never

Now you can use ChildClassOf<> like this:

let constructorOfSomething: ChildClassOf<typeof Something>;
// let constructorOfSomething: new () => Something

Note that the constructor type is typeof Something, which is only possible because Something is the name of the class constructor value.

Playground link to code

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

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.