injectable

A typed token with matching inject and provide functions, so you skip the InjectionToken boilerplate. Think of it as createContext for Angular.

@mmstack/dinpm

#Basic usage

injectable<T>(name) returns a [inject, provide, token] tuple. Name the first two at the call site, provide the value where you want it, and inject it anywhere below. Without a provider the injector returns null.

import { injectable } from '@mmstack/di';

const [injectLogger, provideLogger] = injectable<Logger>('Logger');

// provide it on a component or in a providers array
providers: [provideLogger({ log: (m) => console.log(m) })];

// inject it anywhere below
const logger = injectLogger(); // Logger | null

#Factory providers

provide accepts a factory. Pass a dependency array when the factory needs other services. A zero-argument factory needs no array and can still call inject(), since it runs in an injection context.

const [injectApiConfig, provideApiConfig] = injectable<ApiConfig>('ApiConfig');

// with dependencies
provideApiConfig(
  (http: HttpClient) => ({ baseUrl: '/api', timeout: 5000 }),
  [HttpClient],
);

// zero-arg factory, still allowed to inject()
provideApiConfig(() => ({ baseUrl: inject(BASE_URL), timeout: 5000 }));

#Fallbacks and required values

The options object changes what a missing provider does. Give a fallback for a default value, lazyFallback when the default needs inject() or is expensive, or errorMessage to make the value required. The reader's return type follows: T with a fallback, never null with an error message.

// default value
const [injectTheme] = injectable<Theme>('Theme', {
  fallback: { primary: '#007bff' },
});

// lazily evaluated default (can inject, runs at most once per app)
const [injectTheme2] = injectable<Theme>('Theme', {
  lazyFallback: () => ({ primary: inject(APP_PRIMARY) }),
});

// required, throws if not provided
const [injectApiKey] = injectable<string>('ApiKey', {
  errorMessage: 'Provide it with provideApiKey().',
});

#Providing a function value

provide treats any function as a factory. When the token's type is itself a function, wrap the value in a factory that returns it, or it gets called as one.

type Validator = (value: string) => boolean;

const [injectValidator, provideValidator] = injectable<Validator>('Validator');

provideValidator(() => isLongEnough); // factory returns the function
// provideValidator(isLongEnough);     // would be called as a factory

#The raw token

The third tuple element is the underlying InjectionToken, for deps arrays, Injector.create, or overriding in tests.

const [injectApi, provideApi, API_TOKEN] = injectable<ApiClient>('Api');

// use the token in a classic provider or a test
{ provide: OTHER, useFactory: (api) => new Other(api), deps: [API_TOKEN] }
TestBed.overrideProvider(API_TOKEN, { useValue: fakeApi });