diff --git a/.changeset/true-lemons-happen.md b/.changeset/true-lemons-happen.md new file mode 100644 index 0000000..64575ea --- /dev/null +++ b/.changeset/true-lemons-happen.md @@ -0,0 +1,5 @@ +--- +"@webiny/di": patch +--- + +enforce typechecking of the dependencies array diff --git a/__tests__/container.test.ts b/__tests__/container.test.ts index 275168a..e6554a1 100644 --- a/__tests__/container.test.ts +++ b/__tests__/container.test.ts @@ -193,6 +193,62 @@ describe("DIContainer", () => { expect(numberOfLogCalls).toBe(2); }); + test("should resolve a composite wrapper using manual instances", () => { + let numberOfLogCalls = 0; + + // For assertion purposes, we create a logger decorator. + class LoggerDecorator implements ILogger { + constructor(private decoratee: ILogger) {} + + log(): void { + numberOfLogCalls++; + } + } + + const loggerDecorator = createDecorator({ + abstraction: LoggerAbstraction, + decorator: LoggerDecorator, + dependencies: [] + }); + + const consoleLoggerImpl = createImplementation({ + abstraction: LoggerAbstraction, + implementation: ConsoleLogger, + dependencies: [] + }); + + const compositeImpl = createComposite({ + abstraction: LoggerAbstraction, + implementation: CompositeLogger, + dependencies: [[LoggerAbstraction, { multiple: true }]] + }); + + rootContainer.register(consoleLoggerImpl); + rootContainer.registerDecorator(loggerDecorator); + rootContainer.registerComposite(compositeImpl); + + rootContainer.registerInstance(LoggerAbstraction, new FileLogger()); + + class InjectionTest { + constructor(public logger: ILogger) {} + + getLogger() { + return this.logger; + } + } + + const testObject = rootContainer.resolveWithDependencies({ + implementation: InjectionTest, + dependencies: [LoggerAbstraction] + }); + + const compositeLogger = testObject.getLogger(); + expect(compositeLogger instanceof CompositeLogger).toBe(true); + + compositeLogger.log("Composite log!"); + expect(numberOfLogCalls).toBe(2); + }); + test("should resolve multiple singleton implementations of the same abstraction when multiple flag is used", () => { const consoleLoggerImpl = createImplementation({ abstraction: LoggerAbstraction, diff --git a/__tests__/types.test-d.ts b/__tests__/types.test-d.ts new file mode 100644 index 0000000..aecda45 --- /dev/null +++ b/__tests__/types.test-d.ts @@ -0,0 +1,48 @@ +import { describe, test } from "vitest"; +import { Abstraction } from "~/index.js"; + +interface ILogger { + log(...args: unknown[]): void; +} + +interface IFormatter { + format(message: string): string; +} + +const LoggerAbstraction = new Abstraction("LoggerAbstraction"); +const FormatterAbstraction = new Abstraction("FormatterAbstraction"); + +describe("Container Types", () => { + test("dependencies types must be enforced", () => { + interface IUseCase {} + + const UseCaseAbstraction = new Abstraction("UseCase"); + + class UseCase { + constructor( + private logger: ILogger, + private formatter: IFormatter + ) {} + } + + // Correct assignment!! + UseCaseAbstraction.createImplementation({ + implementation: UseCase, + dependencies: [LoggerAbstraction, FormatterAbstraction] + }); + + // Invalid assignment!! + UseCaseAbstraction.createImplementation({ + implementation: UseCase, + // @ts-expect-error is not assignable to type + dependencies: [LoggerAbstraction] + }); + + // Invalid assignment!! + UseCaseAbstraction.createImplementation({ + implementation: UseCase, + // @ts-expect-error is not assignable to type + dependencies: [LoggerAbstraction, LoggerAbstraction] + }); + }); +}); diff --git a/package.json b/package.json index 2195b11..2ddbbac 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "build": "tsup src/index.ts --format esm --dts", "release": "pnpm run build && changeset publish", "lint": "tsc", - "test": "vitest --run" + "test": "vitest --run --typecheck" }, "dependencies": { "reflect-metadata": "^0.2.2" diff --git a/src/Abstraction.ts b/src/Abstraction.ts index 70fc697..bed7035 100644 --- a/src/Abstraction.ts +++ b/src/Abstraction.ts @@ -9,6 +9,8 @@ type Implementation, I extends Constructor> = I & { // eslint-disable-next-line @typescript-eslint/no-unused-vars export class Abstraction { + // If the generic type is not used in any way, TS simply ignores it, thus breaking the desired type checking. + private readonly __type?: T; public readonly token: symbol; constructor(name: string) {