Playing with Providers
Tags: angular, typescript
Published: 2020-05-17 07:20:12

Dependency injection is one of Angular's most powerful features. Angular Ivy supercharged this with bloom filters and monomorphism while adding more options for `providedIn` to configure the scope of tree-shakable providers.

After digging into Angular, here's 3 things you may not know about providers:

## 1. `inject()`

Under the hood Angular uses code reflection to work out what the dependencies of your classes are. Let's look at an example:

```ts
@Injectable()
export class ApiService {
    constructor(private http: HttpClient) {}
}
```

All well and good. We know that when Angular compiles this code it will detect the `HttpClient` type annotation and wire up `ApiService` so that it is provided with an instance of `Httpclient` when it is constructed by Angular. 

We can write this another way.

```ts
export class ApiService {
    private http = inject(HttpClient)
}
```

This code doesn't use Reflection, so we can omit `@Injectable()`. We are now imperatively fetching the dependency using the service locator pattern. This means class dependencies are hidden within the closure of the class constructor unless we expose it. We can't replace them through the constructor arguments during testing either, but we can still use `TestBed` to configure the injector.

## 2. Injection Context

But why does this work? The documentation says:

> Must be used in the context of a factory function such as one defined for an `InjectionToken`. Throws an error if not called from such a context.

This is interesting, just what exactly qualifies as a factory function? It says we can use it inside `InjectionToken`.

```ts
const IS_BROWSER = new InjectionToken<boolean>("IS_BROWSER", {
    factory() {
        const platformId = inject(PLATFORM_ID)
        return isPlatformBrowser(platformId)
    }
})
```

Well that's convenient. It turns out we can call `inject` inside _any injectable function, class, or factory_. Thats means everything that goes in the `providers` arrays.

```ts
const GLOBAL = new InjectionToken<any>("GLOBAL")

const FACTORY_PROVIDER = {
    provide: GLOBAL,
    useFactory: () => {
        return inject(IS_BROWSER) ? window : global
    }
}
```

Whenever Angular injects a value, it uses a context aware injector that knows where it is in the injector hierarchy. We can create this context ourselves.

```ts
const spy = fn()

class Context {
    constructor() {
        spy(inject(INJECTOR))
    }
}

const parent = Injector.create({
    providers: [Context]
})

const child = Injector.create({
    parent,
    providers: [Context]
})

child.get(Context)

expect(spy).toHaveBeenCalledWith(child)

child.get(Context, null, InjectFlags.SkipSelf)

expect(spy).toHaveBeenCalledWith(parent)
```

## 3. Type Safety

Let's return to the documentation to see what else it says about `inject()`.

> Within such a factory function, using this function to request injection of a dependency is faster and more type-safe than providing an additional array of dependencies (as has been common with `useFactory` providers).

Faster and more type safe. That's great! It's also much more ergonomic when dealing with factory providers, value providers, or generics.

```ts
const API_KEY = new InjectionToken<string>("API_KEY")

@Injectable()
class ApiService {
    constructor(@Inject(API_KEY) private apiKey: string) {}
}
```

This code is not type safe! The constructor is not checked to ensure that `string` matches the signature of `@Inject(API_KEY)`. It's also verbose.

```ts
class ApiService {
    private apiKey = inject(API_KEY) // inferred as string
}
```

For generics we can write a wrapper function.

```ts
function injectStore<T>(store = Store): Store<T> {
    return inject(store)
}

class NgService {
    private store = injectStore<AppState>() // inferred as Store<AppState>
}
```

## Caveats 

> Only call `inject()` synchronously inside the factory/constructor!

You might be thinking, what about components? Sadly, components, directives and pipes (ie. `declarations`) are not created inside an injection context.

```ts
@Component()
export class NgComponent {
    // Error: inject() must be called from an injection context
    private http = inject(HttpClient) 
}
```

There are also issues when trying to inject abstract types, such as `ChangeDetectorRef`.

```ts
export class NgService {
    // Type error, no matching overload
    private cdr = inject(ChangeDetectorRef) 
}
```

This actually works at runtime but the type signature only allows concrete classes.

If you're feeling adventurous there is a workaround to both of these issues with the help of private APIs.

```ts
import { ɵɵdirectiveInject, Type, AbstractType, InjectionToken, ChangeDetectorRef } 
    from "@angular/core"

export function inject<T>(
    token: Type<T> | AbstractType<T> | InjectionToken<T>,
    flags?: InjectFlags,
): T {
    return ɵɵdirectiveInject(token as any, flags)
}

@Component()
export class NgComponent {
    // it works!
    private cdr = inject(ChangeDetectorRef) 
}
```

`ɵɵdirectiveInject` is the injector used internally by Angular when compiling your components and directives into `defineXXX` factories.

```ts
class SomeDirective {
    constructor(directive: DirectiveA) {}

    static ɵdir = ɵɵdefineDirective({
        type: SomeDirective,
        factory: () => new SomeDirective(ɵɵdirectiveInject(DirectiveA))
    });
 }
```

-----

Does this sort of thing interest you? I'm looking for contributors to help build a [Composition API for Angular](https://github.com/stupidawesome/ng-effects).

Thanks for reading!