angular: Better error message when using a service with a generic parameter

I’m submitting a … (check one with “x”)

[ *] bug report => search github for a similar issue or PR before submitting
[ ] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

When compiling a Ionic2 application for AOT, the ngc compiler gives me this error.

ngc: Error: Error at C:/dev/ecdt-mobile/.tmp/app/app.module.ngfactory.ts:177:20: Generic type 'BaseService<T>' requires 1 type argument(s).

Expected behavior

It should compile. I have created an angular2 repro (without ionic) and it compiles fine. HOWEVER the generated code is underline in my IDE and I believe this is not right:

 __BaseService_76:import46.BaseService;

it should be something like: __BaseService_76:import46.BaseService<any>;

Minimal reproduction of the problem with instructions

The repro project is here: https://github.com/graphicsxp/angular2

What is the motivation / use case for changing the behavior?

Please tell us about your environment:

Windows, npm

  • Angular version: 2.0.X 2.0
  • Browser: [all | Chrome XX | Firefox XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ] chrome
  • Language: [all | TypeScript X.X | ES6/7 | ES5] typescript 2.0
  • Node (for AoT issues): node --version =
    v4.5.0

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 2
  • Comments: 27 (13 by maintainers)

Most upvoted comments

hi, I’m getting the same error as @graphicsxp… Actually, @alxhub, I think the repo he posted doesn’t reference Ionic, and the error is reproducible. The type argument is absent for generics in the TS code of the ngfactory. That’s what is causing the issue.

Why DI works with generics when I don’t use AOT ?

It works as long as you have only one instantiation of the generic used as a provider token. If you have two, they will both use the same token and one will obscure the other. In other words, it only appears to work. As soon as you try to introduce another instance of BaseService<T>, your application will subtly break. The code you moved to above works regardless of the number of derived classes from BaseService<T>.

The only way we could support this would be to have a way of reifying a generic type. TypeScript currently does not provide a way to do this and I am not aware of any plans to change this.

@alxhub You are right. Removing BaseService from the list of providers was all I had to do to make it compiles. BUT now it crashes at runtime with the following error: No Provider for BaseService

error_handler.js:47 EXCEPTION: Uncaught (in promise): Error: Error in ./RequestListComponent class RequestListComponent_Host - inline template:0:0 caused by: **No provider for BaseService!**ErrorHandler.handleError @ error_handler.js:47next @ application_ref.js:272schedulerFn @ async.js:82SafeSubscriber.__tryOrUnsub @ Subscriber.js:223SafeSubscriber.next @ Subscriber.js:172Subscriber._next @ Subscriber.js:125Subscriber.next @ Subscriber.js:89Subject.next @ Subject.js:55EventEmitter.emit @ async.js:74NgZone.triggerError @ ng_zone.js:278onHandleError @ ng_zone.js:257t.handleError @ polyfills.js:3e.runGuarded @ polyfills.js:3r @ polyfills.js:3i @ polyfills.js:3invoke @ polyfills.js:3
error_handler.js:52 ORIGINAL STACKTRACE:

That 's why I added BaseService to the list of providers… Any idea what I should do ?

Leaving this issue open; I assigned it to myself and will interpret this to be a request for a better error message as recommended by @alxhub.

There is something that I don’t understand and I would very much like an answer:

Why DI works with generics when I don’t use AOT ?

In the meantime, here is the code I ended up writing. Since DI cannot be used with generics, I’ve followed above recommendations and used inheritance:

export interface IBase {
    id: number;
}

@Injectable()
export abstract class BaseService<T extends IBase> {
  private _loadingService: LoadingService;
  public serviceUrl
  constructor(private _http: Http, @Inject(LoadingService) loadingService: LoadingService) {    
    this._loadingService = loadingService;
  }

  getAll(): Observable<T[]> {
    this._loadingService.presentLoading();
    return this._http.get(this.serviceUrl)
      .map((response: Response) => <T[]>response.json())
      .finally(() => this._loadingService.hideLoading())
      .do(data => console.log("All: " + JSON.stringify(data)))
      .catch(this.handleError);
  }
}

Then in the inherited class:

@Injectable()
export class RequestService extends BaseService<IRequest> {

  constructor(_http: Http, @Inject(LoadingService) loadingService: LoadingService) {
    super(_http, loadingService);
    this.serviceUrl = './build/requests.json';
  }

  getAll(): Observable<IRequest[]> {
    return super.getAll();
  }
}

and in app.module providers: [RequestService]

What I don’t like about this solution is that I have to inject dependencies in both the base class and in the inherited classes. But at least it works with AOT.

@graphicsxp I believe you are taking my comment as a general indictment of generics which is not what I intended at all. There are some implementation challenges that make supporting generics in this context very difficult. It is this difficulty that I was trying to convey, not any slight to generics which I find very, very useful.

I believe the problem with the AuthRestApiService above is that it is inheriting the constructor of RestApiService but the metadata produced for AuthRestApiService declares its constructor doesn’t take any parameters. Try repeating the constructor calling the super constructor as a work-around.

The reason

providers: [
  MyProvider, 
  MyOtherProvider,
  RestApiService<MyPayloadA>
]

doesn’t work is because RestApiService<MyPayloadA> is not valid TypeScript because, in this context, RestApiService<MyPayload> is interpreted as RestApiService < MyPayload comparing the two types instead of using MyPayload as a type parameter (which results in false at runtime as you noticed) and the trailing > is reported a a syntax error and is elided in the output.

The instantiations of a generic types are not reified but are erased following a Java like erasure model. This means that there is no way, at runtime, to distinguish between RestApiService<MyPayloadA> and RestApiService<MyPayloadB> as both translate into a reference to the function RestApiSerivice; so even if this was valid TypeScript we couldn’t support it. Creating a subclass that binds the type parameter creates a concrete reified class that can be used as a token for dependency injection.

DI only supports using concrete reified types as tokens as it must manufacture an instance of the type and the constructor function must be unique to the type. The token cannot be an instantiated generic type because it is ambigious as the type parameters are discarded at runtime and the same constructor function is used for all instantiations.

@BrainCrumbz I had a look at the other issue in your link. That sentence made me jump off my chair:

There are some technical challenges that must be overcome and some TypeScript features that need to be added for this to be implemented correctly that we currently don’t believe adding this support provides enough value to justify the cost of its implementation.

If generics is not a TypeScript feature that provides value, I don’t know what it is then… Or maybe I’ve missed something ?

@liquidboy I believe what you are running into is more along the lines of the general principal requested in #12398. What it proposes is to be able to infer generic parameters from the types of the corresponding inputs. The issue uses NgFor as an example but it should work in your case too. What you want, I believe, is for T to be inferred to be the type of the child component used as the dialog instance.

I just hit this issue with our AOT compiled Angular 2 app … was working perfectly (or what appeared to work perfectly) for the JITed version…

We are injecting ModalDialogInstance < T > in alot of areas, and T can be whatever “Component” we want to load in the ModalDialogInstance … Sadly seems like this is causing AOT to fail because as mentioned above the compiler has no idea what to make of T…

sigh!!!

@chuckjaz, is there a way for the compiler/metadata extractor to give a better error message when attempting to use a class with a generic type parameter as an injection token, instead of the current situation where the generated NgFactory code is incorrect and causes the TypeScript error given in the issue’s title?

Tried also declaring specific classes inheriting from generic one, and registering them as providers (here one of those as an example):

@Injectable()
export class AuthRestApiService extends RestApiService<MyAuthPayload>{
}

providers: [
  MyProvider, 
  MyOtherProvider,
  AuthRestApiService
]

No error highlighted in IDE, but when compiling and building:

ERROR in [default] D:/myproj/codegen/src/app/module.ngfactory.ts:xxx:yyy Supplied parameters do not match any signature of call target.

Looking at factory code:

this._AuthRestApiService_65 = new import43.AuthRestApiService();
                              --------------------------------- highlighted

Error hint in popup says:

[ts] Supplied parameters do not match any signature of call target. constructor AuthRestApiService<import46.AuthPayload>(http: import112.Http): import43.AuthRestApiService

So it looks like type parameter is ignored when _new_ing the service instance.