Minimal type-safe dependency injection framework for TypeScript, inspired by Cake Pattern in Scala.
# npm
npm i --save @susisu/hokemi
# yarn
yarn add @susisu/hokemi
# pnpm
pnpm add @susisu/hokemi
First, declare components of your application.
import type { Component } from "@susisu/hokemi";
export type Clock = {
getTime: () => number;
};
export type ClockComponent = Component<"clock", Clock>;
export type Random = {
getRandom: () => number;
};
export type RandomComponent = Component<"random", Random>;
export type MyService = {
getTimeAndRandom: () => [number, number];
};
export type MyServiceComponent = Component<"myService", MyService>;
Each component has a name (e.g. "clock"
for ClockComponent
), which is used later to reference its instance.
Next, provide implementations for the components.
import { impl } from "@susisu/hokemi";
export const clockImpl = impl<ClockComponent>("clock", () => ({
getTime: () => Date.now(),
}));
export const randomImpl = impl<RandomComponent>("random", () => ({
getRandom: () => Math.random(),
}));
export const myServiceImpl = impl<MyServiceComponent, [ClockComponent, RandomComponent]>(
"myService",
({ clock, random }) => ({
getTimeAndRandom: () => [clock.getTime(), random.getRandom()],
})
);
The impl
function is a utility function to create an implementation of a component. It takes the component name and a factory function that creates an instance of the component.
impl
also takes an optional second type argument (e.g. [ClockComponent, RandomComponent]
for myServiceImpl
), which specifies the dependencies of the implementation. Dependencies are passed to the factory function when the implementation is instantiated.
Finally, mix your implementations and create an instance of the application.
import { mixer } from "@susisu/hokemi";
const app = mixer(myServiceImpl, clockImpl, randomImpl).new();
console.log(app.myService.getTimeAndRandom()); // => [<time>, <random>]
You can reference each component instance by its name (e.g. app.myService
).
If you forget to provide some dependencies, or provide mismatched dependencies, it will be detected at compile time with neat error messages.
const app = mixer(myServiceImpl, clockImpl).new();
// ~~~
// TS2349: This expression is not callable.
// Type '{ __missingDependenciesError?: { reason: "some dependencies are missing"; providerName: "myService"; dependencies: [{ name: "random"; expectedType: Random; }]; } | undefined; }' has no call signatures.
Declares a component.
Creates an implementation of a component.
factory
can be either a function or a class that creates an instance of the component.
Creates a mixer object, which has the following methods:
.with(...implementations)
: extends the mixer with more implementations..new()
: creates a mixed instance.
Adding a type annotation will solve the problem.
import { Mixer, mixer } from "@susisu/hokemi";
// Don't forget `as const`!
const impls = [myServiceImpl, clockImpl, randomImpl] as const;
export const myMixer: Mixer<[...typeof impls]> = mixer(...impls);