netdjw
netdjw

Reputation: 5997

How to create an instance from a generic type in TypeScript?

I have a service what can handle datas in the localstorage and I have an other service what handle the remote API querys. When I define a data model I give it the model is use the local storage or not.

Now I try to build a proxy service what can decide based on the model definition to ask datas from localstorage or from the remote API.

Right now I have this code:

export class ProxyService<T> {
  public type: new () => T;
  private emptyModel: T = new this.type();
  private localstorageService: LocalstorageDataService<T> =
    new LocalstorageDataService(this.type, this.httpClient);
  private remoteDataService: RemoteDataService<T> = new RemoteDataService(this.type, this.httpClient);

  constructor(
    private httpClient: HttpClient,
  ) { }

  getOne(id: number): Observable<T> {
    if (this.emptyModel.use_localstorage) {
      return of(this.localstorageService.findById(id));
    } else {
      return this.remoteDataService.getOne(id).pipe(map((result: T) => result));
    }
  }

  // other proxy functions are here...
}

But I get this error:

Error: Uncaught (in promise): TypeError: this.type is not a constructor
TypeError: this.type is not a constructor
    at new ProxyService (proxy.service.ts:19)

Both RemoteDataService & LocalstorageDataService use a model, so the ProxyService is the best place where I define one. Because the ProxyService is used in multiple place in the whole project with multiple models.

By the way the 19th line is this code from above:

private emptyModel: T = new this.type();

I tried to fix it in the constructor:

export class ProxyService<T> {
  // ...
  constructor(
    public type: new () => T;
    private httpClient: HttpClient,
  ) { }
  // ...

But in this case I need to define a model when I call the ProxyService, and I don't want to define a model every case when I use the ProxyService. In this case I can't use the ProxyService like the next code, but this is the way what I want to use:

export class HelperService<T> {
  constructor(
    private proxy: ProxyService<T>,
  ) { }

Any idea how can I fix this issue?

Upvotes: 2

Views: 9340

Answers (1)

Martin
Martin

Reputation: 16292

You seem to be confusing the TypeScript type annotations with runtime JavaScript concrete implementations.

In TypeScript you cannot do a new T(), because T is just a type annotation and has no concrete implementation.

Your type field undefined so naturally private emptyModel: T = new this.type(); will fail.

I think you were onto the right solution.

Create the Proxy class not as a singleton, but as a class.

Then create a service that exposes a factory method for returning an instance.

export class ProxyFactoryService {

    constructor(private httpClient: HttpClient) { }

    proxyFactory<T>(newable: new() => T) {
      const instance = new newable(); 
      return new ProxyService<T>(this.httpClient, instance);
    }
}

Then your ProxyService can look something like:

 export class ProxyService<T> {
   private localstorageService: LocalstorageDataService<T>;
   private remoteDataService: RemoteDataService<T>;

   constructor(private httpClient: HttpClient, private instance: T) {
     this.localstorageService = new LocalstorageDataService(this.instance, this.httpClient);
     this.remoteDataService = new RemoteDataService(this.instance, this.httpClient)
   }

   getOne(id: number): Observable<T> {
     if (this.instance.use_localstorage) { // we probably want to make sure T extends {use_localStorage: boolean}
       return of(this.localstorageService.findById(id));
     } else {
       return this.remoteDataService.getOne(id).pipe(map((result: T) => result));
    }
  }
  // other proxy functions are here...
}

To use this

constructor(private proxyService: ProxyService) {}

ngOnInit() {
  const proxy = this.proxyService.proxyFactory(SomeClass);
  proxy.getOne(...);
}

Always ask yourself the question: How would this work with annotations removed?

When TypeScript compiles all type annotations are removed -- these are just design time static type annotations.

Another way to think of it is this simplification -- we will remove the generics out and use concrete types. But in essence this boils down what you were trying to do:

const s: new () => String;
const i = new s();

So s is a constructor that when called returns a type String. This will not work because s is undefined. We know what the type it is supposed to be, but in fact it is undefined. This is all very contrived -- but I hope this helps.

If we look at the compiler output for the above, it should be more clear why this wont work.

const s;
const i = new s();

TypeScript luckily will error before we ever actually get a chance to run this code. If your IDE is setup right you should get lots of warnings and errors at design time.

I understand this can be frustrating and confusing, especially coming from languages like C# and Java where the CIL/CLR is statically type aware. In TypeScript there is no runtime, it is just compiled to JavaScript.

Static typing and type annotations are not available at runtime.

Upvotes: 7

Related Questions