Last active
May 2, 2023 11:34
-
-
Save davidwhitney/a6e33e7b24c23eef5ecba804d950b717 to your computer and use it in GitHub Desktop.
TypeScript Really Simple DI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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