Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AoT error with @Inject and interface type #12631

Closed
jeffbcross opened this issue Oct 31, 2016 · 40 comments · Fixed by #14894
Closed

AoT error with @Inject and interface type #12631

jeffbcross opened this issue Oct 31, 2016 · 40 comments · Fixed by #14894
Assignees

Comments

@jeffbcross
Copy link
Contributor

I'm submitting a ... (check one with "x")

[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

I have a constructor that uses @Inject to get a dependency, and references an interface for that the dep, constructor( @Inject(FirebaseApp) _fbApp: firebase.app.App). When I compile the lib with ngc, I get this error:

Error: angularfire2/src/auth/firebase_sdk_auth_backend.ts:27:1: Error encountered in metadata generated for exported symbol 'FirebaseSdkAuthBackend':
 angularfire2/src/auth/firebase_sdk_auth_backend.ts:30:45: Metadata collected contains an error that will be reported at runtime: Expression form not supported.
  {"__symbolic":"error","message":"Expression form not supported","line":29,"character":44}
    at angularfire2/node_modules/@angular/tsc-wrapped/src/collector.js:514:27
    at Array.forEach (native)
    at validateMetadata (angularfire2/node_modules/@angular/tsc-wrapped/src/collector.js:502:42)
    at MetadataCollector.getMetadata (angularfire2/node_modules/@angular/tsc-wrapped/src/collector.js:374:17)
    at MetadataWriterHost.writeMetadata (angularfire2/node_modules/@angular/tsc-wrapped/src/compiler_host.js:111:51)
    at MetadataWriterHost.writeFile (angularfire2/node_modules/@angular/tsc-wrapped/src/compiler_host.js:103:19)
    at Object.writeFile (angularfire2/node_modules/typescript/lib/typescript.js:45364:132)
    at Object.writeFile (angularfire2/node_modules/typescript/lib/typescript.js:7597:14)
    at writeEmittedFiles (angularfire2/node_modules/typescript/lib/typescript.js:37854:20)
    at doEmit (angularfire2/node_modules/typescript/lib/typescript.js:37714:17)

If I change the type to any, compilation succeeds.

Expected behavior

I would expect to be able to reference an interface in a constructor in conjunction with @Inject.

Minimal reproduction of the problem with instructions

ngc this thing:

import { NgModule, Inject, Injectable } from '@angular/core';
import { Http } from '@angular/http';

export interface Foo {}

@Injectable()
export class MyClass {
  constructor(@Inject(Http) http: Foo) {}
}

@NgModule({
  providers: [MyClass]
})
export class MyModule {}
  • Angular version: 2.0.X

@angular/compiler-cli@2.1.2
@angular/core@2.1.2

  • Node (for AoT issues): node --version =

Node 5.10.1

@vicb
Copy link
Contributor

vicb commented Oct 31, 2016

/cc @chuckjaz

@chuckjaz chuckjaz self-assigned this Nov 1, 2016
@filipesilva
Copy link
Contributor

filipesilva commented Nov 14, 2016

I am also experiencing this with @types/angular-router while upgrading an app with @angular/upgrade/static:

@Component({
  // ...
})
export class PhoneDetailComponent {
  // ...
  constructor(@Inject('$routeParams')
                $routeParams: angular.route.IRouteParamsService,
              phone: Phone) {
// ...

Further details in #12860.

@psaussure
Copy link

Same here by using
@Inject(FirebaseApp) private firebaseApp: firebase.app.App

in my constructor. I confirm the @jeffbcross error and agree we shouldn't have to type using any to get the intended behavior.

@ValentinFunk
Copy link

Getting the same thing

export interface LazyMapsAPILoaderConfigLiteral {
  /**
   * The Google Maps API Key (see:
   * https://developers.google.com/maps/documentation/javascript/get-api-key)
   */
  apiKey?: string;

  /**
   * The Google Maps client ID (for premium plans).
   * When you have a Google Maps APIs Premium Plan license, you must authenticate
   * your application with either an API key or a client ID.
   * The Google Maps API will fail to load if both a client ID and an API key are included.
   */
  clientId?: string;
}


@Injectable()
export class LazyMapsAPILoader extends MapsAPILoader {
  private _scriptLoadingPromise: Promise<void>;

  constructor(
    @Inject(LAZY_MAPS_API_CONFIG) private _config: LazyMapsAPILoaderConfigLiteral,
  ) {
    super();
    this._config = this._config || {};
  }
//...
}

Fails with


Failed on type {"filePath":"C:/Dev/trvlm8s/frontend/src/app/services/map/lazy-maps-api-loader.ts","name":"LazyMapsAPILoader"} with error Error: Error encountered resolving symbol value
s statically. Could not resolve type LazyMapsAPILoaderConfigLiteral (position 106:52 in the original .ts file), resolving symbol LazyMapsAPILoader in C:/Dev/trvlm8s/frontend/src/app/se
rvices/map/lazy-maps-api-loader.ts
Error: Error encountered resolving symbol values statically. Could not resolve type LazyMapsAPILoaderConfigLiteral (position 106:52 in the original .ts file), resolving symbol LazyMaps
APILoader in C:/Dev/trvlm8s/frontend/src/app/services/map/lazy-maps-api-loader.ts

@mmc41
Copy link

mmc41 commented Dec 28, 2016

Getting the same thing with a test component (AOT really miss a feature to exclude things):

export const TEST_CONFIG_TOKEN = new OpaqueToken('TestConfig');

export interface TestConfig {
  ...
};

@Component({
  template: `
    ...
  `
})
export class TestHostComponent {
  constructor(@Inject(TEST_CONFIG_TOKEN) public config: TestConfig) {
  }
}

The component is only used for testing but still AOT insist it should be defined in a module (otherwise another error will occur) and when processing AOT produces this error:

ERROR in Error encountered resolving symbol values statically. Could not resolve type TestConfig (position 100:57 in the original .ts file), resolving symbol TestHostComponent ...

A complete blocker - could not be more serious. Only way forward is to delete all my advanced tests in order to get AOT working... NO

@DzmitryShylovich
Copy link
Contributor

DzmitryShylovich commented Dec 28, 2016

Only way forward is to delete all my advanced tests

but u can just change type to any... or change interface to abstract class

@mmc41
Copy link

mmc41 commented Dec 28, 2016

Thanks. This workaround works too. Annoying that I lose type safety here but at least I can proceed.

@DzmitryShylovich
Copy link
Contributor

Annoying that I lose type safety

I believe u can do something like

export class TestHostComponent {
 config: TestConfig;
  constructor(@Inject(TEST_CONFIG_TOKEN) config: any) {
       this.config = <TestConfig>config;
  }
}

@mmc41
Copy link

mmc41 commented Dec 28, 2016

Yes indeed. I actually did exactly what you suggest with an additional comment referencing this bug to explain why I had to write bad code like this (it hurts to write 'any', to do type conversions and to add seemingly unnecessary lines of code ;-))

@oliverjanik
Copy link

Wow this took a long time to find on Google. This is still happening in 2.4.1 using @ngtools/webpack npm package. Why does AOT even care about constructor arguments?

@chuckjaz
Copy link
Contributor

In this case it shouldn't. In general, if no @Inject() is provided, the type of the argument is the type of the instance created by the injector. This bug is tracking that non-reified (such as interfaces) are handled correctly by the collector and the static reflector.

@itsnotvalid
Copy link

Provider Alias also depend of @Inject() which produces the same error.

@Meligy
Copy link
Contributor

Meligy commented Jan 21, 2017

I am having this as well.

This fails with AoT compiler in Angular CLI, because Window (the browser window) is an interface:

export function getWindow() { return window; }

@NgModule({
  declarations: [
    AppComponent,
    SimpleRouteComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    AppRoutingModule
  ],
  providers: [
    {provide: 'window', useFactory: getWindow}
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
  constructor(@Inject('window') w: Window) {
    console.log(w);
  }
}

With error:

ERROR in Error encountered resolving symbol values statically. Could not resolve type Window (position 29:36 in the original .ts file), resolving symbol AppModule in /Users/Meligy/Code/github/Meligy/routing-angular-cli/src/app/app.module.ts

(More details in #14050)

The workaround (other than using any type in the constructor) was to create a dummy class type to trick the compiler, like:

@Injectable()
export class WindowWrapper extends Window {

}

export function getWindow() { return window; }

@NgModule({
  declarations: [
    AppComponent,
    SimpleRouteComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    AppRoutingModule
  ],
  providers: [
    {provide: WindowWrapper, useFactory: getWindow}
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
  constructor(w: WindowWrapper) {
    console.log(w);
  }
}

@gund
Copy link

gund commented Jan 24, 2017

Still happening on v2.4.4. Trying to inject angular's DOCUMENT token:

import { Inject, Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/platform-browser';

@Injectable()
export class MyService {
  constructor(
    @Inject(DOCUMENT) doc: Document // <-- This fails with ngc =(
  ) { }
}

@nosachamos
Copy link

I'm also facing this attempting to inject Window. This is a major pain for real-world apps.

@dscheerens
Copy link

dscheerens commented Feb 23, 2017

Our team is facing the same issue. Rather than solving it by (temporarily) changing the type for such parameters to any we decided to create temporary 'companion' classes for these interfaces, so we don't loose type safety. For example:

export interface MyConfig {
    foo: string;
    bar: boolean;
    baz(a: number): Date;
}

abstract class MyConfigClass implements MyConfig {
    foo: string;
    bar: boolean;
    baz(a: number): Date;
}

@Injectable()
export class MyService {
    // Use MyConfigClass here instead of the MyConfig interface
    constructor(@Inject(MY_CONFIG_TOKEN) config: MyConfigClass) {
    }
}

This ain't pretty and has the drawback of having to repeat the interface definition again in the (abstract) class, but at least it is type safe.

@Meligy
Copy link
Contributor

Meligy commented Feb 23, 2017

@dscheerens see my version of this in this #12631 (comment). In TypeScript, a class can say it extends an interface rather than implements it, which does not require the class to repeat the interface members.

@dscheerens
Copy link

dscheerens commented Feb 23, 2017

@Meligy I actually tried that, but that only seems to work for the Window interface. If I do this for my own interface, TypeScript is complaining that you cannot extend an interface:

[ts] Cannot extend an interface 'MyConfig'. Did you mean 'implements'?

I also tried to mimic the same behavior from the Window interface by doing this:

declare var MyConfig: {
    prototype: MyConfig;
    new(): MyConfig;
}

That works to get rid of the cannot extend an interface message, but then NGC again gives me an error that it cannot resolve MyConfig :(

@Meligy
Copy link
Contributor

Meligy commented Feb 23, 2017

I went for some help from Twitter to find out why. Here's how to do it:

interface RealInterface {
  title: string;
}

// This does the trick, a `var`, with `new` and `prototype`
declare var RealInterface: {
    prototype: RealInterface;
    new(): RealInterface;
};

class FakeClass extends RealInterface {
}

Live Sample

Thanks @dscheerens. I just learned something new :)

@dscheerens
Copy link

@Meligy That works to for the extend stuff, but you'll end up with the initial AoT compilation error. So unfortunately it doesn't help as a workaround for the AoT issues.

@sod
Copy link
Contributor

sod commented Feb 23, 2017

It was nice if typescript would export something for interfaces the angular DI could work with. But until then you can use abstract instead of interface:

https://plnkr.co/edit/xzGHB54LVOb2If6mJfZl?p=preview

abstract class AbstractFooService {
  abstract fooProperty: string;
}

class FooService implements AbstractFooService {
  fooProperty = 'string from service'
}

@Component({
  providers: [{provide: AbstractFooService, useClass: FooService}],
  selector: 'my-app',
  template: `{{ fooService.fooProperty }}`
})
export class AppComponent {
  constructor(public fooService: AbstractFooService) {
  }
}

The angular team does the same right now - e.g. the ReflectorReader provided by that abstract token.

If everything is abstract in the abstract class, all that is left is an empty function after transpilation to es5.

@skdhir
Copy link

skdhir commented Mar 1, 2017

Thanks @DzmitryShylovich for referencing my issue here.
Reading the comments, I'm not sure if you guys found the workaround / solution

@DzmitryShylovich
Copy link
Contributor

@skdhir for example:

private window: Window;
constructor(@Inject('Window') window: any) {
   this.window = window as Window;
}

@matejamateusz
Copy link

@DzmitryShylovich Yep. I mean I'm having this issue for Ionic 2 - having function in app.module.ts:

export function httpInterceptor(backend: XHRBackend, defaultOptions: RequestOptions, events: Events) { return new HttpInterceptor(backend, defaultOptions, **window**, events); }

which in httpInterceptor.ts :

@Injectable() export class HttpInterceptor extends Http { constructor( backend: ConnectionBackend, defaultOptions: RequestOptions, public window: Window, public event: Events ) { super(backend, defaultOptions); }

Now the ngc is saying me: Could not resolve type Window (position 11:22 in the original .ts file), resolving symbol HttpInterceptor...

@DzmitryShylovich
Copy link
Contributor

@matejamateusz remove @Injectable() from `HttpInterceptor. You don't need it if you use factory provider.

And don't extend Http. Create a wrapper.

@matejamateusz
Copy link

@DzmitryShylovich What do you mean by wrapper?

chuckjaz added a commit to chuckjaz/angular that referenced this issue Mar 11, 2017
@mmc41
Copy link

mmc41 commented Mar 11, 2017

@chuckjaz Is this fixed in v4 ? If so, I have a good reason for upgrading :-)

@DzmitryShylovich
Copy link
Contributor

not yet

@mmc41
Copy link

mmc41 commented Mar 11, 2017

@DzmitryShylovich Thanks for the info. It seems whenever I have a angular question/issue, you provide a good answer within 5 minutes. Very impressive. I can't state enough how much your help is appreciated!

@matejamateusz
Copy link

matejamateusz commented Mar 11, 2017 via email

@DzmitryShylovich
Copy link
Contributor

#14894 the fix

@MadUser
Copy link

MadUser commented Mar 30, 2017

Are you sure this works with ng build --prod?
because for me it still breaks

@DzmitryShylovich
Copy link
Contributor

@MadUser it does work using plain ngc. If it doesn't work in cli then it's a cli issue

@mmc41
Copy link

mmc41 commented Mar 30, 2017

Works fine for me with angular cli 1.0 and angular 4.0 project

@DzmitryShylovich
Copy link
Contributor

@MadUser looks like it doesn't work with ts interfaces such as Window, Document etc. See #15640

devversion added a commit to devversion/material2 that referenced this issue Apr 24, 2017
Due to a bug (angular/angular#12631 (comment)), specifiying a type for a injected provider didn't work in AOT mode.

This bug has been solved, but we still need to switch to a `interface` instead of a TypeScript `type`.
mmalerba pushed a commit to angular/components that referenced this issue Apr 25, 2017
Due to a bug (angular/angular#12631 (comment)), specifiying a type for a injected provider didn't work in AOT mode.

This bug has been solved, but we still need to switch to a `interface` instead of a TypeScript `type`.
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 11, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.