Simple Type-safety for Angular Inject

Nicholas Favero
4 min readOct 29, 2020

Angular’s DI is absolutely fantastic, but with all things, it is always fun to see if we can improve on what is already there. One of the things that always bothered me about the token injection is the lack of Typescript types that went along with it. It allows for a lot of flexibility that can end up biting you in larger projects.

For those of you who are familiar with @Inject() in Angular, know that you can use this to provide DI into different Angular Features. If you are not familiar with this concept, you can find more information at

https://angular.io/guide/dependency-injection-in-action

We will be going through some simple examples of DI and evolving the process as we go.

For those of you who want to just see some of this in action, you can visit https://stackblitz.com/edit/angular-ivy-mpyozi?file=src/app/inject-types.type.ts. Check out the inject-types.type.ts, the tokens folder, services folder, and the app.module.ts

One of the things that you’ll notice is that in order to assign a type in your constructor, you have to know what type that Token will be providing. For example:

const NAME_TOKEN = new InjectionToken<{ firstName: string; lastName: string; }>(‘NAME TOKEN’);

@Injectable() class MyProvider {

constructor(@Inject(NAME_TOKEN) name: { firstName: string; lastName: string}) {}

}

Now you could make this a little bit cleaner by changing the inline definition to an interface

export interface NameToken { firstName: string; lastName: string; }

const NAME_TOKEN = new InjectionToken<NameToken>(‘NAME TOKEN’);

@Injectable() class MyProvider {

constructor(@Inject(NAME_TOKEN) name: NameToken) {}

}

While this is a better solution than the first, it has always felt a bit strange to me that we have to know the token and the type that the token is supposed to supply, and leaves room for error. Using typescript we can actually simplify this so that we do not need to know the interface the Token uses explicitly.

To do this, we will create a couple utility types that we can import for the rest of our project.

export type SingleInject<TIn> = TIn extends InjectionToken<infer R> ? R : unknown;

export type MultiInject<TIn> = TIn extends InjectionToken<infer R> ? R[] : unknown;

export type InjectProvider<TIn> = TIn extends InjectionToken<infer R> ? R : unknown;

Let us go back to our original example and see how we can use the utility types now

const NAME_TOKEN = new InjectionToken<{ firstName: string; lastName: string; }>(‘NAME TOKEN’);

@Injectable() class MyProvider {

constructor(@Inject(NAME_TOKEN) name: SingleInject<typeof NAME_TOKEN>) {}

}

This is a bit nicer, as our Token is now our single source of type truth, but this still leaves a bit to be desired, first of all, you have to have prior knowledge of whether this token is a single or multi provider token. And without looking through documentation, or knowing what the author wants for it, it can cause some headaches. So let’s extend upon our utility types.

export type InjectSettings<TMulti extends boolean, TIn extends InjectionToken<any> = InjectionToken<any>> = TIn & { multi?: TMulti; };

export type SingleInject<TIn> = TIn extends InjectionToken<infer R> ? R : unknown;

export type MultiInject<TIn> = TIn extends InjectionToken<infer R> ? R[] : unknown;

export type InjectType<TIn> = TIn extends InjectSettings<true, infer R> ? MultiInject<R> : TIn extends InjectSettings<false, infer R> ? SingleInject<R> : unknown;

export type InjectProvider<TIn> = TIn extends InjectSettings<boolean, InjectionToken<infer R>> ? R : TIn extends InjectionToken<infer R> ? R : unknown;

const createSettings = <TMulti extends boolean>(multi: TMulti) => <TIn extends InjectionToken<any>>(token: TIn): InjectSettings<TMulti, TIn> => {
(token as InjectSettings<TMulti, TIn>).multi = multi;
return token;
};

export const MultiInjectProvider = createSettings(true);

export const SingleInjectProvider = createSettings(false);

So the above can be a bit confusing, but just bear with me for now. These new utility types can now be used to extend our token creation. So let’s take a look at our example from before with these new classes.

const NAME_TOKEN = MultiInjectProvider(new InjectionToken<{ firstName: string; lastName: string; }>(‘NAME TOKEN’));

@Injectable() class MyProvider {

constructor(@Inject(NAME_TOKEN) name: InjectType<typeof NAME_TOKEN>) {}

}

This is much nicer, now I no longer have to know about if the service is a single source, or a multi, because my token creator provided me with that information by attaching it to their token. Which means we can actually use this information for providing our implementation of this token. An example of this is

@Injectable() class NameProvider implements InjectProvider<typeof NAME_TOKEN> {
firstName = ‘John’;
lastName = ‘Doe’;
}

@NgModule({

providers: [ProvideInject(NAME_TOKEN, NameProvider)]

})
export class SampleModule {}

The above code creates Type-safety for your implementation and for registering your provider with a module.

To see this in action you can visit https://stackblitz.com/edit/angular-ivy-mpyozi

Hope this helps!

About Me:
My name I am the CTO and Co-Founder of mTreatment LLC. We believe in creating great technology to bring health care together and empower the patient. I love all things Angular & Typescript, and love being able to share new tips & tricks as we discover them.

https://www.mtreatment.com
https://health.mtx-app.com

--

--