Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to provide a service that uses generics in Angular4?

I notice that my project has a lot of resolver services that despite from working with different entities and repositories host the same code. So I took the challenge to reduce them to a single resolver service using generics:

@Injectable()
export class DetailResolver<T, R extends Repository<T>> implements Resolve<T> {

  constructor(private repositoryService: R, private router: Router) {}

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T> {
    // use repositoryService to resolve this route
  }
}

The entity type T and the repository type R can be specified. However I have trouble using this service:

const appRoutes: Routes = [
  // other routes
  {
    path: ':id',
    component: MessageEditComponent,
    resolve: {
      message: DetailResolver<Message, MessageService>
    }
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(appRoutes)
  ],
  exports: [
    RouterModule
  ],
  providers: [
    DetailResolver<Message, MessageService>
  ]
})

As soon as I specify the generic type in DetailResolver<Message, MessageService> the compiler wants me to create an instance:

Value of type typeof DetailResolver is not callable. Did you mean to include new?

I am not really familiar with angular4 DI internals. Can anybody describe whats wrong, is there any solution to this problem? I am using angular 4.3.

like image 609
UpCat Avatar asked Oct 12 '17 09:10

UpCat


1 Answers

When you register a provider with an injector, the DI system uses that token—or what could be otherwise seen as a key—to maintain a token-provider map that is referenced every time that a dependency is asked for.

Tokens are generally objects that are unique and symbolic, so the concept of generics directly conflicts in this area precisely because the token is going to function as a key. Generics are a design-time artifact, which is another way of saying that they will disappear from the generated JavaScript and therefore won't leave their type information for Angular to find at runtime. TypeScript interfaces are also not valid tokens for the same reason.

Abstract classes

My first option would be to use abstract classes. Abstract classes solve two problems here: 1) While they cannot be instantiated directly, unlike an interface they can contain implementation details and therefore can be compiled into Javascript. 2) You obtain a DI token via an architecture based on extending, which might work very well for your architecture.

In your case you could do something like the following:

@Injectable()
export abstract class DetailResolver<T, R extends Repository<T>> implements Resolve<T> {...}

@Injectable()
export class MessageResolver extends DetailResolver<Message, MessageService> {...}

And then in the NgModule you would provide it in the following manner:

providers: [{ provide: DetailResolver, useClass: MessageResolver }]

InjectionToken

Another option is the use of InjectionToken (known as OpaqueTokens prior to Angular 4.0). InjectionTokens are objects that are exclusively used as DI tokens; however unlike they're predecessor, OpaqueToken, they require to be typed as to what is the type of value they will inject.

const MESSAGE_RESOLVER = new InjectionToken<DetailResolver<Message, MessageService>>('MESSAGE_RESOLVER');

You'll notice that you can provide the type as you're creating the InjectionToken instance to have the compiler support you along the way. That is enforced when you add the provider in your NgModule.

providers: [{ provide: MESSAGE_RESOLVER, useClass: MessageResolver }]

It's worth noting that useClass is not technically required there; any of the other alternatives such as useValue or useFactory are valid here when providing the corresponding.

Hope this is helpful and provides some clarification on the matter!

like image 159
emarticor Avatar answered Sep 28 '22 12:09

emarticor