Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript: get typeof generic constructor

Tags:

typescript

In TypeScript, I've defined a helper function called create that takes a constructor and the constructor's typed arguments to create a new instance of a class:

function create<Ctor extends new (...args: any[]) => any, R extends InstanceType<Ctor>>(
  ctor: Ctor,
  ...args: ConstructorParameters<Ctor>
): R {
  return new ctor(...args)
}

This works for simple classes like so:

class Bar  {
  constructor(param: number) { }
}

const bar1 = create<typeof Bar, Bar>(Bar, 1);

// Compile error: Argument of type '"string"' is not assignable to parameter of type 'number'.ts(2345)
const bar2 = create<typeof Bar, Bar>(Bar, 'string');

However, if I have a generic class, I can't get TypeScript to perform the proper type checking on create:

class Foo<T>  {
  constructor(param: T) { }
}

// Ok
const foo1 = create<typeof Foo, Foo<number>>(Foo, 1);

// This should be an error but is not
const foo2 = create<typeof Foo, Foo<number>>(Foo, { not: 'a number' })

The root cause is that signature of create in the second case takes an unknown parameter:

enter image description here

Main question

Is it possible to explicitly write out the type of the generic class constructor in the call to create without inlining the type itself and while still preserving the signature of create?

Attempts

My first thought was:

create<typeof Foo<number>, Foo<number>>(Foo, { not: 'a number' })

But that is invalid code.

So far, the best workaround I've found is to use a temporary class to explicitly capture the constructor type:

class Temp extends Foo<number> { }

// This correctly generates an error
create<typeof Temp, Foo<number>>(Foo, { not: 'a number' });

Can this be done in a more elegant way?

like image 756
Matt Bierner Avatar asked Dec 18 '19 01:12

Matt Bierner


People also ask

What is a type type constructor in typescript?

The constructors of class can have types of their own. Above syntax, generic types are used, indicating that the constructor will return an object that can be a class instance of type T. Using the typed constructor, one can implement the factory design pattern in TypeScript.

How do you create a class type in typescript using generics?

Using Class Types in Generics. When creating factories in TypeScript using generics, it is necessary to refer to class types by their constructor functions. For example, function create < Type > ( c: { new (): Type }): Type {. return new c ();

Why TypeOf is not used for constructors?

The 'typeof' operator is not meant to describe the constructor of a type, but rather the type of a value. SomeGeneric<T> is not a value, it is a type in and of itself.

What are some of the workarounds for TypeScript's type parameters?

Some of the workarounds we found are the followings: Feature suggestion is to allow typeof generic type. typeof takes a value and produces the type that the value has. Generic type parameters are types; they are already types and there's no need to have an operator to convert them to types.


1 Answers

The create() function should probably be generic in the argument list A to the constructor, as well as the constructed instance type R. This takes advantage of higher order type inference that was introduced in TypeScript 3.4.

function create<A extends any[], R>(
    ctor: new (...args: A) => R,
    ...args: A
): R {
    return new ctor(...args)
}

From there, the question is how to specify the types so that you will ask for a Foo<number> and get some sort of error if your parameter is not correct. One way to do this is to manually specify the generic parameters:

// Specify generics
const fooSpecifiedGood = create<[number], Foo<number>>(Foo, 123); // okay
const fooSpecifiedBad = create<[number], Foo<number>>(Foo, { not: 'a number' }); // error

That works. If, as you mentioned, your constructor parameter list is too long or complicated to write out, then I'd suggest just annotating the type of a variable to which you save the returned instance, like this:

// Just annotate the variable you're writing to
const fooAnnotatedGood: Foo<number> = create(Foo, 123); // okay
const fooAnnotatedBad: Foo<number> = create(Foo, { not: 'a number' }); // error

Now possibly you'd prefer being able to call create<Foo<number>>(Foo, 123) where you manually specify the R parameter as Foo<number>, but let the compiler infer the A parameter. Unfortunately TypeScript doesn't currently support such partial type parameter inference. For any given generic function call, either you can manually specify all the type parameters, or you can let the compiler infer all the type parameters, and that's basically it (type parameter defaults complicate the story a bit but it still doesn't give you this ability). One workaround for this is to use currying to take a single function like <A, R>()=>... and turn it into a higher order function like <A>()=><R>()=>.... Applying this technique to create() gives you this:

// Or get a curried creator function that lets you use specify the instance type
// while allowing the compiler to infer the compiler args
const curriedCreate = <R>() =>
    <A extends any[]>(ctor: new (...args: A) => R, ...args: A) => new ctor(...args);

const fooCurriedGood = curriedCreate<Foo<number>>()(Foo, 123); // okay
const fooCurriedBad = curriedCreate<Foo<number>>()(Foo, { not: 'a number' }); // error

So those are the main options I see. Personally I'd use the fooAnnotated approach, since it is analogous to const oops: Foo<number> = new Foo({not: 'a number'}). The error isn't in calling the Foo constructor; it's in assuming that the result is a Foo<number>. Anyway, hope one of those helps you. Good luck!

Playground link to code

like image 52
jcalz Avatar answered Sep 21 '22 16:09

jcalz