Skip to content

Instantly share code, notes, and snippets.

@davidwhitney
Last active May 2, 2023 11:34
Show Gist options
  • Save davidwhitney/a6e33e7b24c23eef5ecba804d950b717 to your computer and use it in GitHub Desktop.
Save davidwhitney/a6e33e7b24c23eef5ecba804d950b717 to your computer and use it in GitHub Desktop.
TypeScript Really Simple DI
import { Container, Inject } from "./Container";
describe("Container", () => {
it("should be able to get a class", () => {
const container = new Container();
container.register(TestClass);
const result = container.get<TestClass>(TestClass);
expect(result).toBeInstanceOf(TestClass);
expect(result.foo).toBe("bar");
});
it("should be able to get a class with a dependency", () => {
const container = new Container();
container.register(TestClass);
container.register(TestClassWithDep);
container.register(SomeDep);
const result = container.get<TestClassWithDep>(TestClassWithDep);
expect(result).toBeInstanceOf(TestClassWithDep);
expect(result.foo.foo).toBe("bar");
});
it("should be able to get a class with a factory dependency", () => {
const container = new Container();
container.register(SomeDepWhichNeedsAFactory, { using: () => new SomeDepWhichNeedsAFactory("abc") });
const result = container.get<SomeDepWhichNeedsAFactory>(SomeDepWhichNeedsAFactory);
expect(result).toBeInstanceOf(SomeDepWhichNeedsAFactory);
expect(result.foo).toBe("abc");
});
it("should support providing registrations", () => {
const container = new Container();
container.register(SomeDepWhichNeedsAFactory, { using: () => new SomeDepWhichNeedsAFactory("abcd") });
const result = container.get<SomeDepWhichNeedsAFactory>(SomeDepWhichNeedsAFactory);
expect(result).toBeInstanceOf(SomeDepWhichNeedsAFactory);
expect(result.foo).toBe("abcd");
});
it("should support providing values", () => {
const container = new Container();
container.register(SomeDepWhichNeedsAFactory, new SomeDepWhichNeedsAFactory("abcdj"));
const result = container.get<SomeDepWhichNeedsAFactory>(SomeDepWhichNeedsAFactory);
expect(result).toBeInstanceOf(SomeDepWhichNeedsAFactory);
expect(result.foo).toBe("abcdj");
});
it("should support providing factory functions", () => {
const container = new Container();
container.register(SomeDepWhichNeedsAFactory, () => new SomeDepWhichNeedsAFactory("abcde"));
const result = container.get<SomeDepWhichNeedsAFactory>(SomeDepWhichNeedsAFactory);
expect(result).toBeInstanceOf(SomeDepWhichNeedsAFactory);
expect(result.foo).toBe("abcde");
});
it("should error when key provided without value", () => {
const container = new Container();
container.register("foo");
expect(() => {
container.get("foo");
}).toThrow("Registration found for 'foo' but no value was provided");
});
});
class TestClass {
public foo: string;
constructor() {
this.foo = "bar";
}
}
class SomeDep {
public foo: string;
constructor() {
this.foo = "bar";
}
}
class SomeDepWhichNeedsAFactory {
public foo: string;
constructor(fooValue: string) {
this.foo = fooValue;
}
}
class TestClassWithDep {
public foo: SomeDep;
constructor(@Inject("SomeDep") someDep: SomeDep) {
this.foo = someDep;
}
}
// Requires the following flags in tsconfig.json:
// "experimentalDecorators": true
// "emitDecoratorMetadata": true
type Constructor = { new (...args: any[]) };
export type Registration = {
usingConstructor?: Constructor,
using?: any | (() => any),
}
const typeConstructionRequirements = new Map<string, any[]>();
export function Inject(registrationName: any) {
return (target: any, __: string, paramIndex: number) => {
if (!typeConstructionRequirements.get(target.name)) {
typeConstructionRequirements.set(target.name, []);
}
const metadata = typeConstructionRequirements.get(target.name);
metadata.push({ paramIndex, registrationName });
typeConstructionRequirements.set(target.name, metadata);
}
}
export class Container {
public registrations = new Map<string, Registration>();
public register(key: string | Constructor, value?: any | (() => any)) {
const ctorProvided = typeof key !== "string";
const keyProvided = typeof key === "string";
if (ctorProvided && !value) {
value = { usingConstructor: key };
}
if (keyProvided && !value) {
value = {
using: () => { throw new Error(`Registration found for '${key}' but no value was provided`); }
};
}
const registrationKey = ctorProvided ? (key as Constructor).name : key;
const valueOrFactoryProvided = !value.usingConstructor && !value.using;
if (valueOrFactoryProvided) {
value = { using: value };
}
this.registrations.set(registrationKey, value);
}
public get<T>(key: Constructor | string): T {
const ctorProvided = typeof key !== "string";
const registeredKey = ctorProvided ? (key as Constructor).name : key;
return this.getByKey(registeredKey);
}
public getByKey<T>(key: string): T {
if (!this.registrations.get(key)) {
throw new Error("No registration found for key: " + key);
}
const registration = this.registrations.get(key);
if (registration.using && typeof registration.using === "function") {
return registration.using() as T;
}
if (registration.using) {
return registration.using as T;
}
const metadata = typeConstructionRequirements.get(key) || [];
metadata.sort((a, b) => a.paramIndex - b.paramIndex);
const args = [];
for (const metadataItem of metadata) {
const value = this.getByKey(metadataItem.registrationName);
args.push(value);
}
return new registration.usingConstructor(...args);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment