Hi All,
I wanted to use the IoC Dependency Injection (DI) pattern on a little project I am working on in TypeScript, and I did not want to use any libraries, just plain TypeScript. I came up with this pattern below, which is not ideal, but gets close to a proper IoC DI paradigm I think.
For the sake of the explanation, let's assume we have two interfaces IFoo
and IBar
and the corresponding implementations CFoo
and CBar
. We also have CApp
as the main class. Let's assume CApp
depends on IFoo
and CFoo
depends on IBar
. We want to recursively instantiate all dependencies starting from the CApp
class. Below is what the ideal syntax would look like:
interface IBar {
bar();
}
interface IFoo {
foo();
}
class CBar implements IBar {
bar() {
console.log("Hello, world");
}
}
class CFoo implements IFoo {
bar: IBar;
constructor(bar: IBar) {
this.bar = bar;
}
foo() {
this.bar.bar();
}
}
class CApp {
foo: IFoo;
constructor(foo: IFoo) {
this.foo = foo;
}
}
const app = ioc.resolve(CApp);
Unfortunately, this exact syntax is not possible, because IFoo
and IBar
types do not exist at runtime. My alternative is to use a kind of type ID with syntactic sugar.
interface IBar {
bar();
}
const IBar = {} as IBar; // type ID
class CFoo implements IFoo {
static CFoo = { // static object
bar: IBar // bar is assigned the value of `const IBar`
};
bar: IBar;
constructor(deps: typeof CFoo.CFoo) {
this.bar = deps.bar;
}
}
What we did here is we declared a type ID constant with the same name as the type. The constant is a reference to some unique object cast to the type it represents. In the class where dependencies are to be injected, we declared a static object of the same name as the class, so we can dynamically look it up. The object's declaration colon (:
) syntax conveniently makes it look like a type, while in reality, this is an assignment of the type ID value. This static object is essentially a mini reflection metadata without a reflection library. We can use typeof
to infer the type of the object that TypeScript will enforce when you call the constructor.
With this setup, the IoC container looks like this:
class IoC {
di = new Map<object, Function>();
constructor() {
this.di.set(IFoo, () => this.resolve(CFoo));
this.di.set(IBar, () => this.resolve(CBar));
}
resolve<T>(ctor: new (arg?: {}) => T): T {
const className = ctor.name;
const deps = (ctor as any)[className];
if (deps) {
const resolvedDeps = Object.fromEntries(
Object.entries(deps).map(
([key, typeId]) => [key, this.di.get(typeId as object)()]
)
);
return new ctor(resolvedDeps);
}
return new ctor();
}
}
const ioc = new IoC();
The dependency resolvers are defined directly in the constructor of the IoC. The resolve<T>()
method takes a class as an argument, determines the name of the static property which lists the dependencies, and resolves them by calling the factory function in the resolvers map. The factory function, in turn, recursively resolves its own type, in case it also has dependencies.
With this syntax in place, and the implementation of the IoC container, all you need to do to bootstrap your app is
const app = ioc.resolve(CApp);
Happy coding!
Top comments (0)