Search code examples
typescriptgenericsinstance

Is there a way to instantiate an instance of a constrained generic type in TypeScript?


I'm coming to you because I've exhausted all of the Google recommendations. I'm trying to create a websocket system that will handle statically typed requests and responses.

I'm generating the request/response types from another project. I have two base types (RequestData and ResponseData) and a "GenericRequest" that the user will interact with when trying to send new requests. An example looks like this:

export class RequestData {}
export class ResponseData {}

export class GenericRequest<T extends RequestData, V extends ResponseData> {}
interface HandshakeResponseData extends ResponseData {
    AuthCode: string;
    RequestId: number; //unsigned
    RedirectUrl: string;
}

interface HandshakeRequestData extends RequestData {
    AuthCode: string;
    RequestId: number; //unsigned
    SessionToken: string;
}

export class HandshakeRequest extends GenericRequest<HandshakeRequestData, HandshakeResponseData> {}

Here is an example of it's usage.

new HandshakeRequest({
    AuthCode: "0",
    SessionToken: "<jwt>"
}).send(socket).then((data) => {
    // response handler logic
});

There is a function within GenericRequest<T,V> called send() that sends a request off and waits for a response. When the response comes back, it is supposed to convert the response json into a ResponseData instance of type V. I'm having trouble creating the new instance of ResponseData from the generic argument V:

private create<R> (type: { new(): R} ) : R {
        return new type();
}

^ This is the method of instantiating a generic type I've seen all over Stack Overflow, but it doesn't seem to work for me when I do this:

foo() : V { // where V corresponds to generic argument of GenericRequest<T,V>
    return this.create<V>(V);
}

I get an error on the parameter within the parenthesis:

'V' only refers to a type, but is being used as a value here. ts(2693)

Here is the most minimum reproducible sample I could think of. I'd like to instantiate a new V object where V in Foo<V>

class Foo<V> {
    constructor() {

    }

    create<V>(type: { new(): V} ) : V {
        return new type();
    }

    thisIsTheError() {
        let bar: V = this.create<V>(V);
    }
}

Solution

  • TypeScript's type system is erased when the code is transpiled into Javacript. At runtime, you can't call this.create(V) or new V because V does not exist. (That's what the error is telling you). The solution you see elsewhere about requiring a parameter of type new()=>V is meant to indicate that, at runtime, you need an actual constructor object that makes instances of type V. Such a constructor object doesn't come into existence magically; anything that needs one of those will have to get it from somewhere. In your small Foo example, I'd say it should be passed into the Foo constructor, like this:

    class Foo<V> {
    
      // pass in the V constructor here:
      constructor(private ctor: new () => V) {
    
      }
    
      create<V>(type: { new(): V }): V {
        return new type();
      }
    
      thisIsTheError() {
        let bar: V = this.create(this.ctor); // okay now
        return bar; // return this just to show what it does
      }
    }
    

    Of course that means you can't just call new Foo() anymore.

    const badFoo = new Foo(); // error, needs an argument
    

    You'll need to pass it a no-arg constructor of the type you want your Foo to make. For example:

    class Baz {
      constructor(public name: string = "Fred") { }
      yell() {
        console.log("HEY MY NAME IS " + this.name.toUpperCase() + "!!!")
      }
    }
    
    const foo = new Foo(Baz); 
    const baz = foo.thisIsTheError(); // okay, type is Baz
    baz.yell(); // HEY MY NAME IS FRED!!!
    

    You can see that foo is inferred to be of type Foo<Baz> since you pass it the no-arg Baz constructor. (Baz does allow you to new it with no arguments... the name property will become the default value "Fred"). And then foo.thisIsTheError() successfully instantiates the Baz class by calling its constructor with no argument. I made thisIstheError() return the bar value so you could see that it really is a class instance, and has the yell() method.

    Okay, hope that helps; good luck!

    Link to code