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
🐞 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
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
added
the
P5
The team acknowledges the request but does not plan to address it, it remains open for discussion
label
Oct 30, 2020
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()); } }
Copy link
Contributor
Author
Copy link
Contributor
Author
I have Angular 11 and Ivy enabled
Is it a new (empty) ng project?
Copy link
Contributor
Author
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 { }
Copy link
Contributor
Author
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);
};
}
};
};
};
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.
Copy link
Contributor
Author
@splincode updated my previous comment — not much added to be honest.
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;
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:
as you can see:
-
DI works without
@Injectable
inside custom decorator
-
returned class is the original one (not wrapped)
It appears this behaves as expected. If you still feel there is an issue, please provide further details in a new issue.
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