Reputation: 28272
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
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)
Upvotes: 4
Views: 1585
Reputation: 13574
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>,
) {}
}
Upvotes: 3