4

I'd like to be able to inject a service with a generic constrained to an interface in Angular, using a common factory... I can do it if I declare a provider of every injected type, but that defeats the purpose of why I want it this way.

What I want is something like:

interface WorkerServiceContract {
  doWork(): void;
}

class MyService<T extends WorkerServiceContract> {
  constructor(private worker: T) {}

  doWorkWrapper() { this.worker.doWork(); }
}

So at any point I can do a:

@Injectable({ providedIn: 'root' })
class FooWorkerService implements WorkerServiceContract {
   doWork() { console.log('foo'); }
}

@Injectable({ providedIn: 'root' })
class BarWorkerService implements WorkerServiceContract {
   doWork() { console.log('bar'); }
}

@Component(/*blabla*/)
class MyComponent {
   constructor(private fooWorker: MyService<FooWorkerService>, private barWorker: MyService<BarWorkerService>) {}
}

I understand I can declare the injection using specific tokens for each of the WorkerServiceContract possibilities, but I'm wondering if there's a way (I've looked through the documentation but couldn't figure it out), to have it "open"... so I could do something like (this wouldn't obviously work):

providers: [
   {
      provide: MyService<T extends ServiceWorker>
      useFactory: (worker: T) => new MyService<T>(worker);
      deps: [T]
   }
] 

I understand that's not possible in the provider definition (since it doesn't know T), but is there any mechanism which would allow something "generic" like this to work? It's probably something obvious but I can seem to get my head around to do it

I'm using Angular 9


Real-Life scenario (addendum)

The whole rationale about why we want this (the real-life scenario) is:

I have a tool-generated service class (from Swagger/OpenApi). For that service, I create a proxy/wrapper service that intercepts all the http (not http, but the methods, which do more than just calling the http client) calls to the API and pipes the returned observables to handle errors (and successes, actually) to show UI notifications (and create other calls to diagnostic logging servers, etc.).

These handlers are sometimes are generic, but each view (depending on the called API) may want to have their own handlers (e.g., one may show a toast, one may show a popup, one might want to transform the call to the API in some ways, or whatever).

I could do it on every call to the API, but on my team, we've found out that separating those concerns (the handling of a successful call with normal data received from the API, and the handling of errors) helps both the readability and size of the code of the components (and the responsibility of each code). We've already solved that by a "simple" call on the constructor, e.g.:

constructor(private apiService: MyApiService) {
  this.apiService = apiService.getProxyService<MyErrorHandler>();
}

Which returns a Proxy that handles all that. This works just fine, but we were discussing the idea of making it "even cleaner", like so:

constructor(private apiService: MyApiService<MyErrorHandler>) {}

And have a factory on the DI container create that proxy for us, which would bring the benefit of both: a) not having to remember doing that call on the constructor, and b) have a clear view of ALL the dependencies (including the error handler) directly on the constructor parameters (without having to dig into the constructor code, which could have other stuff depending on the actual component)

And no, an HttpClient interceptor wouldn't work here, since the auto-generated service does more than the HttpClient call (and we want to work on what gets returned from that service, not directly on the HttpResponse object)

1 Answer 1

3

UPDATED

Perhaps to try an injector with tokens

const services = new Map();
const myService = <T>(service: T): InjectionToken<MyService<T>> => {
  if (services.has(service)) {
    return services.get(service);
  }

  const token = new InjectionToken<<T>(t: T) => MyService<T>>(`${Math.random()}`, {
    providedIn: 'root',
    factory: injector => new MyService(injector.get(service)),
    deps: [Injector],
  });

  services.set(service, token);
  return token;
};

class MyComponent {
   constructor(
     @Inject(myService(FooWorkerService)) private foo: MyService <FooWorkerService>,
     @Inject(myService(BarWorkerService)) private bar: MyService <BarWorkerService>,
   ) {
  }
}

ORIGINAL

You are right, generics aren't present after transpilation and therefore can't be used as providers.

To solve it you need to inject factory itself in your component. Because anyway you would specify type of the generic, now you need bit more code to achieve desired behavior.

you can use


const MY_SERVICE_FACTORY = new InjectionToken<<T>(t: T) => MyService<T>>('MY_SERVICE_FACTORY', {
    providedIn: 'root',
    factory: () => worker => new MyService(worker),
});

// just a helper to extract type, can be omitted
export type InjectionTokenType<Type> = Type extends InjectionToken<infer V> ? V : never;

class MyComponent {
   constructor(
     @Inject(MY_SERVICE_FACTORY) private serviceFactory: InjectionTokenType<typeof MY_SERVICE_FACTORY>,
     foo: FooWorkerService,
     bar: BarWorkerService,
   ) {
   this.fooWorker = this.serviceFactory(foo);
   this.barWorker = this.serviceFactory(bar);
  }
}

also to keep the code cleaner in the constructor you can move it to the providers of the component.

@Component({
  // blabla
  providers: [
    {
      provide: 'foo',
      useFactory: t => new MyService(t),
      deps: [FooWorkerService],
    },
    {
      provide: 'bar',
      useFactory: t => new MyService(t),
      deps: [BarWorkerService],
    },
  ],
})
class MyComponent {
   constructor(
     @Inject('foo') private fooWorker: MyService<FooWorkerService>,
     @Inject('bar') private barWorker: MyService<BarWorkerService>,
   ) {}
}
Sign up to request clarification or add additional context in comments.

15 Comments

I was afraid something like this was the only way. The current way I solve it is having the MyService have a kind of getActualService<T>(): T method which I call on the constructor (and acts as my factory), without defining a specific provider. This is what I wanted to avoid through DI (avoid having to do extra steps on the constructor of the type using the service, I mean), but yes, doesn't look like it's possible :-/ I'll +1 your answer for now and wait to see if other answers come in. Thanks!
Thanks! I've added one more example how to keep code cleaner in the class by moving things to providers, but anyway it requires usage factory for every type.
I've edited and added a "real-life scenario" part where I explain why we want this, just in case it could bring more ideas up. Again thank you for your time :-)
Unfortunately, no luck on my side. I came up with @Inject(findService(FooWorkerService)) private fooWorker: MyService<FooWorkerService>, but it always requires to specify @Inject.
Yup... I basically could make it already by having a @WithHandler(MyService, Handler) private service: MyService parameter... now I want to remove the redundant MyService in the decorator, but finding the type of the parameter is proving hard... I'm working on it on my "break time". Will post with the final solution when I get it :-)
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.