Error this constructor was not compatible with dependency injection

In this article, you'll fix the error "This constructor is not compatible with Angular Dependency Injection" when working with Jest for unit tests.

In this short article, you’ll learn how to fix this annyoing issue in Angular when you’re starting
to implement component unit tests with Jest that have dependencies. Or you might be just migrating
away from Jasmine and Karma.

What the error?

If you have an Angular component that depends on a service, and you’ve just migrated to Jest from Jasmine and Karma,
you might see something as follows if you’ve missed one critical step:

This constructor is not compatible with Angular Dependency Injection because its dependency at index 0 of the parameter list is invalid.
    This can happen if the dependency type is a primitive like a string or if an ancestor of this class is missing an Angular decorator.

    Please check that 1) the type for the parameter at index 0 is correct and 2) the correct Angular decorators are defined for this class and its ancestors.

      at ɵɵinvalidFactoryDep (../packages/core/src/di/injector_compatibility.ts:112:9)

Example:

Let’s suppose we have a component named WatchComponent as follows:

import { Component, OnInit } from '@angular/core'
import { WatchService } from 'src/app/services/watch.service'

@Component({
  selector: 'app-watch',
  templateUrl: './watch.component.html',
  styleUrls: ['./watch.component.scss'],
})
export class WatchComponent implements OnInit {
  time: string

  constructor(private watchService: WatchService) {}

  ngOnInit(): void {
    this.time = this.watchService.getTime()
  }

  startTimer() {
    this.watchService.startTimer()
  }

  stopTimer() {
    this.watchService.stopTimer()
  }

  resetTimer() {
    this.watchService.resetTimer()
  }
}

And we have the WatchService as follows:

import { Injectable } from '@angular/core'

@Injectable({
  providedIn: 'root',
})
export class WatchService {
  constructor() {}

  getTime() {
    return new Date()
  }

  startTimer() {
    //some code
  }

  stopTimer() {
    //some code
  }

  resetTimer() {
    //some code
  }
}

If you write the tests as follows, you’ll still see the error we’ve mentioned above:

import { ComponentFixture, TestBed } from '@angular/core/testing'
import { WatchService } from 'src/app/services/watch.service'

import { WatchComponent } from './watch.component'

describe('WatchComponent', () => {
  let component: WatchComponent
  let fixture: ComponentFixture<WatchComponent>
  let watchServiceStub: Partial<WatchService>
  let watchService

  beforeEach(async () => {
    watchServiceStub = {
      startTimer: () => {},
      stopTimer: () => {},
      resetTimer: () => {},
    }
    await TestBed.configureTestingModule({
      declarations: [WatchComponent],
      providers: [{ provide: WatchService, useValue: watchServiceStub }],
    }).compileComponents()
  })

  beforeEach(() => {
    fixture = TestBed.createComponent(CounterComponent)
    component = fixture.componentInstance
    fixture.detectChanges()
    watchService = TestBed.inject(WatchService)
  })

  it('should get the inital value of time from the service on component init', () => {
    component.ngOnInit()
    expect(watchService.getTime).toBeCalled()
  })
})

Solution?

The reason why the tests work with Karma and Jasmine, and not with Jests is just one tiny
thing missing from one of the tsconfig files that some of the articles online have missed. For example, while working on the #ngBook I’m writing as of today, I followed this article and ended up
with the mentioned issue.

If you’ve faced the same, just make sure that you have set both the esModuleInterop and emitDecoratorMetadata properties to true inside your tsconfig.spec.json file.
It should ideally look something as follows:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/spec",
    "types": ["jest", "node"],
    "esModuleInterop": true,
    "emitDecoratorMetadata": true
  },
  "files": ["src/polyfills.ts"],
  "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
}

The esModuleInterop property makes sure you don’t get any weird warnings from Jest while running the tests. And the emitDecoratorMetadata makes sure that the decorator metadata information is compatible and available beween Angular’s Dependency Injection and Jest.

Conclusion

Go through different articles/sources while implementing a solution 😄.
If you liked the article, considering subscribing to the newsletter for more. And share this article on your social links with 🖤.
And as always, Happy Coding! 🙌🏼

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


Closed

splincode opened this issue

Sep 24, 2020

· 17 comments

Labels

area: core

Issues related to the framework runtime

core: di

P5

The team acknowledges the request but does not plan to address it, it remains open for discussion

Comments

@splincode

🐞 bug report

Affected Package

The issue is caused by package @angular/compiler

Is this a regression?

Yes, since everything worked fine in previous versions (Angular 8, Angular 9)

Description

In my projects I use decorators and now for some reason the error is constantly shown, what does this mean and how to get around it? Is this a bug in Angular? The decorator is ordinary and made according to the TypeScript guide

import { Component, VERSION, Injectable } from '@angular/core';

// simple example
export function Decorator(): any {
    return function <T extends any>(parent: any) {
        return class extends parent {
            constructor(...args: any[]) {
                super(...args);
            }
        };
    };
}

@Decorator()
@Injectable()
class MyService {
  public world() {
    return 'world';
  }
}

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ],
  providers: [MyService]
})
export class AppComponent  {
  name = 'Angular ' + VERSION.major;

  constructor(private my: MyService){
    console.log('hello', my.world())
  }
}

🔬 Minimal Reproduction

https://stackblitz.com/edit/token-invalid?file=src/app/app.component.ts

🔥 Exception or Error


DEPRECATED: DI is instantiating a token "" that inherits its @Injectable decorator but does not provide one itself.
This will become an error in a future version of Angular. Please add @Injectable() to the "" class.

🌍 Your Environment

Angular Version:

Angular 10.1

@jelbourn
jelbourn

changed the title
DEPRECATED: DI is instantiating a token «» that inherits its @Injectable decorator but does not provide one itself. This will become an error in a future version of Angular. Please add @Injectable() to the «» class.

Injectable decorator not recognized when used with custom decorator

Oct 30, 2020

@jelbourn
jelbourn

added
the

P5

The team acknowledges the request but does not plan to address it, it remains open for discussion

label

Oct 30, 2020

@ulrikwalter

Use the Injectable decorator for the returned new class in own Decorator. For example:

import { Component, VERSION, Injectable, InjectableProvider, Type } from "@angular/core";

export function Decorator(injectionOptions?: {
        providedIn: Type<any> | 'root' | 'platform' | 'any' | null;
    } & InjectableProvider): any {
  return function<T extends any>(stateClass: any) {
    class DecoratedClass extends stateClass {
      constructor(...args: any[]) {
        super(...args);
      }
    };
    return Injectable(injectionOptions)(DecoratedClass);
  };
}

@Decorator({ providedIn: 'root' })
export class MyService {
  public world() {
    return "world";
  }
}

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"],
  //providers: [MyService],
})
export class AppComponent {
  name = "Angular " + VERSION.major;

  constructor(private my: MyService) {
    console.log("hello", my.world());
  }
}

@splincode



Copy link


Contributor

Author

@ulrikwalter

@splincode



Copy link


Contributor

Author

I have Angular 11 and Ivy enabled

@ulrikwalter

Is it a new (empty) ng project?

@splincode



Copy link


Contributor

Author

@tonivj5

I think the question here is if we are going to be able to decorate our classes with custom decorators and at the same time be able of use @Injectable(). What are the future plans? I’d like to know this

My use case is to mark a class as final:

export function Final<T extends new (...args: any[]) => Record<any, any>>(target: T): T {
  return class FinalKlass extends target {
    constructor(...args: any[]) {
      if (new.target !== FinalKlass) {
        throw new Error(`Cannot extend a final class "${target.name}"`);
      }
      super(...args);
    }
  };
}

@Final
@Injectable()
export class NotExtensibleService { }

@splincode



Copy link


Contributor

Author

@petebacondarwin

@martinbojnansky

I am experiencing the same problem, however, the bigger issue than warning itself is that such a usage of custom decorator ends up without injected dependencies in test environment.

Is it me, or should this be supported use case?

export const View = () => {
  return <T extends new (...args: any[]) => OnInit & OnDestroy & WithChangeDetector>(constructor: T ) => {
    const unsubscriber = new ObservableUnsubscriber();

    return class extends constructor {
      constructor(...args: any[]) {
        super(...args);

        const ngOnInit = constructor.prototype.ngOnInit;
        constructor.prototype.ngOnInit = () => {
          ...
          ngOnInit.apply(this);
        };

        const ngOnDestroy = constructor.prototype.ngOnDestroy;
        constructor.prototype.ngOnDestroy = () => {
          unsubscriber.destroy();
          ngOnDestroy.apply(this);
        };
      }
    };
  };
};

@tomstolarczuk

For anybody still interested, this works:

export function Decorator(module: string): <T extends new (...args: any[]) => any>(target: T) => T {
  return function decorator<T extends new (...args: any[]) => any>(target: T): T {
    @Injectable()
    abstract class Decorated extends target {
	  someProp = 'someProp';
      // do something with decorated class
    }
    return Decorated;
  };
}


@Injectable({
  providedIn: 'root'
})
@Decorator()
export class SomeApiService {
  constructor(private http: HttpClient) {}

  someMethod(params: any): void {
    console.log(this.someProp);
  }
}

Tested on Angular 12.

@splincode



Copy link


Contributor

Author

@tomstolarczuk

@splincode updated my previous comment — not much added to be honest.

@bvandermeide

For anybody still interested, this works:

export function Decorator(module: string): <T extends new (...args: any[]) => any>(target: T) => T {
  return function decorator<T extends new (...args: any[]) => any>(target: T): T {
    @Injectable()
    abstract class Decorated extends target {
	  someProp = 'someProp';
      // do something with decorated class
    }
    return Decorated;
  };
}


@Injectable({
  providedIn: 'root'
})
@Decorator()
export class SomeApiService {
  constructor(private http: HttpClient) {}

  someMethod(params: any): void {
    console.log(this.someProp);
  }
}

Tested on Angular 12.

You may receive the following error if you try to apply constructor logic

Uncaught (in promise): Error: This constructor was not compatible with Dependency Injection. Error: This constructor was not compatible with Dependency Injection.

If you need to do constructor initialization logic, I found the following to work well.

    abstract class Decorated extends target {
      constructor(...args) {
        super(...args);
        someProp = 'someProp';
      }
    }

    @Injectable()
    class Decorated_Injectable() extends Decorated { }

    return Decorated_Injectable;

@mkurcius

As mentioned here: https://stackoverflow.com/a/53622493/4663231
Proxy and Reflect.construct can be used:

export function MyDecorator() {
  return (target: any) => {
    return new Proxy(target, {
      construct(clz, args) {
        console.log('before constructor');
        const obj = Reflect.construct(clz, args);
        console.log('after constructor');

        return obj;
      },
    });
  };
}

@MyDecorator()
@Injectable({ providedIn: 'root' })
export class MyClass {
  constructor(@Inject(DOCUMENT) private document: Document) {
    console.log('Impl (constructor)');
    console.log('document', this.document);
  }
}

@Injectable({ providedIn: 'root' })
export class MyService {
  constructor(private myClass: MyClass) {
    console.log('myClass', this.myClass);
  }
}

and the output is:
result

as you can see:

  1. DI works without @Injectable inside custom decorator
    result -DI

  2. returned class is the original one (not wrapped)
    result -class

@jessicajaniuk

It appears this behaves as expected. If you still feel there is an issue, please provide further details in a new issue.

@angular-automatic-lock-bot

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.

Labels

area: core

Issues related to the framework runtime

core: di

P5

The team acknowledges the request but does not plan to address it, it remains open for discussion

Issue

I am importing a service in my login page, but when it is giving me this error, i have all the decorators in my service file and i have all the proper imports in my app modules. but i have no idea why is this error still coming. i have been importing services in same way but i dont know what exactly it needs to inject that service in the component.

Uncaught (in promise): Error: This constructor is not compatible with Angular Dependency Injection because its dependency at index 2 of the parameter list is invalid.
This can happen if the dependency type is a primitive like a string or if an ancestor of this class is missing an Angular decorator.

loginPage.ts

import { UserService } from "src/app/services/user.service";

 constructor(
    public navCtrl: NavController,
    private userService: UserService) { }

async doLogin() {
    let loader = await this.loadingCtrl.create({
      message: "Please wait...",
    });
    loader.present();

    this.userService.login(this.account).subscribe(
      (resp) => {
        loader.dismiss();
        this.navCtrl.navigateRoot("");
        //this.push.init();
      })}; 

my userService.ts

import { HttpApiService } from "./httpapi.service";
import { DeviceInfo } from "@capacitor/core";
import { AnalyticsService } from "./analytics.service";

@Injectable({
  providedIn:"root"
})
export class UserService {
  _user: any;

  constructor(
    private api: HttpApiService,
    private al: AnalyticsService,
    private device: DeviceInfo  
  ) {}
// all other methods ....
}

my httpApiService

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";

@Injectable({
  providedIn: "root",
})
export class HttpApiService {
  // LIVE

  constructor(private http: HttpClient) {
    this.url = "my url";
  }

  get(endpoint: string, params?: any, options?): Observable<any> {
    // Support easy query params for GET requests
    if (params) {
      let p = new URLSearchParams();
      for (let k in params) {
        p.set(k, params[k]);
      }
      // Set the search field if we have params and don't already have
      // a search field set in options.
      options.search = (!options.search && p) || options.search;
    }

    return this.http.get(this.url + "/" + endpoint, options);
  }

  post(endpoint: string, body: any, options?): Observable<any> {
    return this.http.post(this.url + "/" + endpoint, body, options);
  }

  put(endpoint: string, body: any, options?): Observable<any> {
    return this.http.put(this.url + "/" + endpoint, body, options);
  }

  delete(endpoint: string, options?): Observable<any> {
    return this.http.delete(this.url + "/" + endpoint, options);
  }

  patch(endpoint: string, body: any, options?): Observable<any> {
    return this.http.put(this.url + "/" + endpoint, body, options);
  }
}

my ionic info

Ionic:

   Ionic CLI                     : 5.2.7 (C:UsersAditAppDataRoamingnpmnode_modulesionic)
   Ionic Framework               : @ionic/angular 5.1.1
   @angular-devkit/build-angular : 0.901.7
   @angular-devkit/schematics    : 9.1.6
   @angular/cli                  : 9.1.7
   @ionic/angular-toolkit        : 2.2.0

Capacitor:

   Capacitor CLI   : 2.1.0
   @capacitor/core : 2.1.0

Utility:

   cordova-res : not installed
   native-run  : not installed

System:

   NodeJS : v10.16.3 (C:Program Filesnodejsnode.exe)
   npm    : 6.11.3
   OS     : Windows 10

Solution

inside my userService.ts i was calling the capacitor plugin from constructor which was wrong and which created the module error as it cant be injected to a component and needs to be declared as a constant as per docs.

my old userService.ts

import { HttpApiService } from "./httpapi.service";
import { DeviceInfo } from "@capacitor/core";
import { AnalyticsService } from "./analytics.service";

@Injectable({
  providedIn:"root"
})
export class UserService {
  _user: any;

  constructor(
    private api: HttpApiService,
    private al: AnalyticsService,
    private device: DeviceInfo  
  ) {}
// all other methods ....
}

my new userService.ts

import { HttpApiService } from "./httpapi.service";
import { AnalyticsService } from "./analytics.service";
import { Plugins } from "@capacitor/core";
const { Device } = Plugins;

@Injectable({
  providedIn:"root"
})
export class UserService {
  _user: any;

  constructor(
    private api: HttpApiService,
    private al: AnalyticsService, 
  ) {}

async getInfo(){
const info = await Device.getInfo();
console.log(info);
}
// all other methods ....
}

Answered By — adit bharadwaj

Issue

I am importing a service in my login page, but when it is giving me this error, i have all the decorators in my service file and i have all the proper imports in my app modules. but i have no idea why is this error still coming. i have been importing services in same way but i dont know what exactly it needs to inject that service in the component.

Uncaught (in promise): Error: This constructor is not compatible with Angular Dependency Injection because its dependency at index 2 of the parameter list is invalid.
This can happen if the dependency type is a primitive like a string or if an ancestor of this class is missing an Angular decorator.

loginPage.ts

import { UserService } from "src/app/services/user.service";

 constructor(
    public navCtrl: NavController,
    private userService: UserService) { }

async doLogin() {
    let loader = await this.loadingCtrl.create({
      message: "Please wait...",
    });
    loader.present();

    this.userService.login(this.account).subscribe(
      (resp) => {
        loader.dismiss();
        this.navCtrl.navigateRoot("");
        //this.push.init();
      })}; 

my userService.ts

import { HttpApiService } from "./httpapi.service";
import { DeviceInfo } from "@capacitor/core";
import { AnalyticsService } from "./analytics.service";

@Injectable({
  providedIn:"root"
})
export class UserService {
  _user: any;

  constructor(
    private api: HttpApiService,
    private al: AnalyticsService,
    private device: DeviceInfo  
  ) {}
// all other methods ....
}

my httpApiService

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";

@Injectable({
  providedIn: "root",
})
export class HttpApiService {
  // LIVE

  constructor(private http: HttpClient) {
    this.url = "my url";
  }

  get(endpoint: string, params?: any, options?): Observable<any> {
    // Support easy query params for GET requests
    if (params) {
      let p = new URLSearchParams();
      for (let k in params) {
        p.set(k, params[k]);
      }
      // Set the search field if we have params and don't already have
      // a search field set in options.
      options.search = (!options.search && p) || options.search;
    }

    return this.http.get(this.url + "/" + endpoint, options);
  }

  post(endpoint: string, body: any, options?): Observable<any> {
    return this.http.post(this.url + "/" + endpoint, body, options);
  }

  put(endpoint: string, body: any, options?): Observable<any> {
    return this.http.put(this.url + "/" + endpoint, body, options);
  }

  delete(endpoint: string, options?): Observable<any> {
    return this.http.delete(this.url + "/" + endpoint, options);
  }

  patch(endpoint: string, body: any, options?): Observable<any> {
    return this.http.put(this.url + "/" + endpoint, body, options);
  }
}

my ionic info

Ionic:

   Ionic CLI                     : 5.2.7 (C:UsersAditAppDataRoamingnpmnode_modulesionic)
   Ionic Framework               : @ionic/angular 5.1.1
   @angular-devkit/build-angular : 0.901.7
   @angular-devkit/schematics    : 9.1.6
   @angular/cli                  : 9.1.7
   @ionic/angular-toolkit        : 2.2.0

Capacitor:

   Capacitor CLI   : 2.1.0
   @capacitor/core : 2.1.0

Utility:

   cordova-res : not installed
   native-run  : not installed

System:

   NodeJS : v10.16.3 (C:Program Filesnodejsnode.exe)
   npm    : 6.11.3
   OS     : Windows 10

Solution

inside my userService.ts i was calling the capacitor plugin from constructor which was wrong and which created the module error as it cant be injected to a component and needs to be declared as a constant as per docs.

my old userService.ts

import { HttpApiService } from "./httpapi.service";
import { DeviceInfo } from "@capacitor/core";
import { AnalyticsService } from "./analytics.service";

@Injectable({
  providedIn:"root"
})
export class UserService {
  _user: any;

  constructor(
    private api: HttpApiService,
    private al: AnalyticsService,
    private device: DeviceInfo  
  ) {}
// all other methods ....
}

my new userService.ts

import { HttpApiService } from "./httpapi.service";
import { AnalyticsService } from "./analytics.service";
import { Plugins } from "@capacitor/core";
const { Device } = Plugins;

@Injectable({
  providedIn:"root"
})
export class UserService {
  _user: any;

  constructor(
    private api: HttpApiService,
    private al: AnalyticsService, 
  ) {}

async getInfo(){
const info = await Device.getInfo();
console.log(info);
}
// all other methods ....
}

Answered By — adit bharadwaj

Понравилась статья? Поделить с друзьями:
  • Error this build is outdated spigot как убрать
  • Error this asset has a trainz build number which is not recognised by this tool
  • Error these packages do not match the hashes from the requirements file
  • Error there was an error with your transaction please try again
  • Error there was an error while processing your request