Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/true-lemons-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@webiny/di": patch
---

enforce typechecking of the dependencies array
56 changes: 56 additions & 0 deletions __tests__/container.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
48 changes: 48 additions & 0 deletions __tests__/types.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<ILogger>("LoggerAbstraction");
const FormatterAbstraction = new Abstraction<IFormatter>("FormatterAbstraction");

describe("Container Types", () => {
test("dependencies types must be enforced", () => {
interface IUseCase {}

const UseCaseAbstraction = new Abstraction<IUseCase>("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]
});
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions src/Abstraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ type Implementation<A extends Abstraction<any>, I extends Constructor> = I & {

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class Abstraction<T> {
// 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) {
Expand Down