4

Update

Intro

In Angular Services are provided using the decorator @Injectable.

@Injectable() // -> works
export class MyService {
  constructor() {}
}

Abstracting @Injectable

Before Ivy, it was possible to build an abstraction for @Injectable (e.g. for configuring the provider dynamically, enhancing the service class).

The following snippet shows an example how @Injectable can be wrapped.

function InjectableEnhanced() {
  return <T extends new (...args: any[]) => InstanceType<T>>(target: T) => {
    Injectable({ providedIn: "root" })(target);
  };
}

Using the decorator InjectableEnhanced (see above) does not work while Ivy is enabled. The following code snipped causes a runtime error.

@InjectableEnhanced() // -> does not work
export class MyService {
  constructor() {}
}

Runtime error

Compiling the service using @InjectableEnhanced with angular/cli works, but the following error is shown in the browser. The corresponding project can be found at https://github.com/GregOnNet/ng-9-inject.git.

enter image description here

Maybe, the Angular compiler does some code transformation but is not able any more to resolve @Injectable inside other decorators. Having a look at the angular repository, a reference to JIT-compiler can be found in injectable.ts (see: https://github.com/angular/angular/blob/master/packages/core/src/di/injectable.ts#L14).

Question

Is there still a way abstracting @Injectable?

Repository for reproduction

https://github.com/GregOnNet/ng-9-inject.git

Jeff Mercado
  • 129,526
  • 32
  • 251
  • 272
Gregor Woiwode
  • 1,046
  • 1
  • 9
  • 12

2 Answers2

4

A custom provider can be created by using some of angular's internal API:

import { ɵɵdefineInjectable, ɵɵinject } from "@angular/core";

export function InjectableEnhanced() {
  return <T extends new (...args: any[]) => InstanceType<T>>(target: T) => {
    (target as any).ɵfac = function() {
      throw new Error("cannot create directly");
    };

    (target as any).ɵprov = ɵɵdefineInjectable({
      token: target,
      providedIn: "root",
      factory() {
        // ɵɵinject can be used to get dependency being already registered
        const dependency = ɵɵinject(Dependency); 
        return new target(dependency);
      }
    });
    return target;
  };
}

Working example can be found at https://github.com/GregOnNet/ng-9-inject

Gregor Woiwode
  • 1,046
  • 1
  • 9
  • 12
  • seems like in Angular 12 `ɵfac` is only a getter and it's impossible to assign a function to it. Any ideas how to overcome this? – Kirill Metrik May 21 '21 at 08:45
  • Hi, I recently updated my library and my library still works. But I will double-check this. – Gregor Woiwode May 24 '21 at 08:46
  • Ok, I verified it too. So it's working with AOT=true. But for non-aot builds with Ivy Angular adds `ɵfac` and `ɵprov` getters which you cannot modify. For this case creating normal `Injectable` instance still works like before. I'd only like to know whether there is a way to know if build is aot or not in runtime... – Kirill Metrik May 24 '21 at 09:30
  • 1
    Ok, so in case someone else has the same problem, here is how to work around it. Instead of `(target as any).ɵprov = ɵɵdefineInjectable` use `Object.defineProperty(target, 'ɵprov', {.....})` – Kirill Metrik May 24 '21 at 13:24
1

The decorator is getting attached to the constructor as expected, but when the AppComponent is created the injector tries to resolve the provider and crashes.

I think the error message is just a generic error for when a component fails construction, but the error is happening when Angular is trying to get the injectables for the AppComponent constructor.

If you log the constructor for the service you can see that the provider metadata has been attached:

@InjectableEnhanced()
export class MyService {
  constructor() {
  }
}


console.log((MyService as object).prototype.constructor.hasOwnProperty('ɵprov'));
// prints "true"

When I try to inspect that property it triggers the error:

try {
  console.log((MyService as object).prototype.constructor.ɵprov);
} catch (err) {
  console.log(err); // prints the same error message
}

I think the property is a getter property that resolves to the instance of the provider and that is what is crashing.

The closest issue on Angular that I could find was this one but it's still open:

https://github.com/angular/angular/issues/31495

So I feel like the Ivy compiler might be searching the source code for @Injectable() and building a list of expected providers, and it doesn't see this new decorator so the MyService is excluded from the list. Later at run-time the metadata for the decorator is there, but the injector doesn't know what it is for and crashes.

I tried to find something documented where you could register a new decorator with the Ivy compiler, but wasn't able to and I don't know if such a thing exist.

FYI: I do this exact same thing on one of my other projects so I think there will be a lot of people effected by this.

Reactgular
  • 52,335
  • 19
  • 158
  • 208
  • Thank you, for your answer. It really helps me to dig deeper. I read https://github.com/angular/angular/issues/31495 and also think this could be related. Nevertheless, I will open another issue since IoC is affected here, too. – Gregor Woiwode Jan 10 '20 at 07:21
  • @GregorWoiwode can you update your question with a link to the issue that you opened so that others can find it. thanks. – Reactgular Jan 10 '20 at 11:13