42

Will Angular inject more than one instance of a generic Service if it is invoked across Component constructors using different Types?

I have a lot of Services that are going to provide the same functionality for different classes that all inherit from the same base class. I'd like to use Typescript's generic pattern MyService<T extends BaseClass> to handle this.

But I don't know how it jives with Angular's injector:

  1. Will the Angular injector create more than one instance of this service if it's injected into different components with different Types?
  2. Will the injector inject the same instance if it is called with the same type?

Code:

@Injectable
export class MyService<T> {
    ...
}

@Component
export class ComponentA {
    constructor(alphaService MyService<Alpha>) {}   <--Instance 1
}

@Component
export class ComponentB {
    constructor(betaService MyService<Beta>) {}   <----Instance 2?
}

@Component
export class ComponentA1 {
    constructor(alphaService MyService<Alpha>) {}  <---Instance 1?
}

Is this legal? Does the injector know to create two instances of MyService?

3 Answers 3

42

This is a simple solution:

// service and models
export class User {
  firstName: string
}

export class Admin {
  lastName: string
}

@Injectable()
export class GenericService<T>{
  item: number = Math.random();
  GetAll(): Array<T> {
    let output = [];
    console.log(this.item); // each instance has own value
    return output;
  }
}

Then set your service in module through useFactory:

providers: [
  { provide: 'UserService', useFactory: () => (new GenericService<User>()) },
  { provide: 'AdminService', useFactory: () => (new GenericService<Admin>()) },
],

and inject your service with @Inject decorator:

constructor(
  @Inject('UserService') private userService: GenericService<User>,
  @Inject('AdminService') private adminService: GenericService<Admin>
) { }

Keep in mind it is better to use InjectionToken ( OpaqueToken is deprecated) for your provider token.I have used string just for simplicity.

Sign up to request clarification or add additional context in comments.

4 Comments

Just a note: OpaqueToken is deprecated. Use InjectionToken instead.
So, how do you inject the HttpClient (or HttpModule) object for constructor like this one constructor(private http:HttpClient)
Use @Inject("UserService") private userService: any. if it requires a lot of work to build the GenericService and all the Typed implementations. In my case I would need to have dozens of Typed classes in order to get started with our upgrade. So I just treated userService as any in order to Inject the angularJS service with type any.
what about class UserService extends GenericService<User>? since UserService is an actual class it could be injected separately, right?
6

An additional instance won't be created for generics. As it is stated here, reflect-metadata (which is used by Angular 2 decorators to support TypeScript type annotations) can't obtain this kind of information:

TypeScript only emits type metadata for types available at run time, and does not emit type aliases, interfaces, or generics (as they do not have a JavaScript representation).

The amount of MyService instances depends on whether MyService was defined as component's provider or it was inherited from parent injector.

In the code above, ComponentA and ComponentA1 can inherit MyService provider from parent injector, ComponentB can get a new instance with providers: [MyService].

Comments

3

You can skip adding a service using a module's provider array, and make it available from the root scope similarly to when you set providedIn to root in a service's @Inject decorator. To do this use an InjectionToken with a factory method and set the providedIn value.

export const MyServiceAlphaToken = new InjectionToken<MyService<Alpha>>(
  'AlphaService', { factory: () => new MyService<Alpha>(), providedIn: 'root' });

export const MyServiceBetaToken = new InjectionToken<MyService<Beta>>(
  'BetaService', { factory: () => new MyService<Beta>(), providedIn: 'root' });

Then just inject with the @Inject parameter decorator. The only issue is that it's not type safe - there's no comparison between the generic parameter specified in the token and the type specific in the service.

export class ComponentA {
  constructor(@Inject(MyServiceAlphaToken) alphaService: MyService<Alpha>) {}   
}

export class ComponentB {
  constructor(@Inject(MyServiceBetaToken) betaService: MyService<Beta>) {}
}

Also note, that there is no deps array like with providers. Instead, use the inject function with the service class or InjectionToken for each dependency.

export const MyServiceWithDepdents = new InjectionToken<MyService<Omega>>('OmegaService', 
  { 
    factory: () => new MyService<Omega>(inject(DepAService), inject(DepBToken)), 
    providedIn: 'root'
  }
);

1 Comment

"The only issue is that it's not type safe" - I would say that this is a very critical point and that no one should wander around without type safety.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.