Skip to content

Commit

Permalink
Merge pull request #51 from owja/token
Browse files Browse the repository at this point in the history
Add support for type-safe token based injection
Published as 2.0.0-alpha.2
  • Loading branch information
Hauke B authored May 27, 2022
2 parents 9750045 + 7320956 commit a39b75f
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 30 deletions.
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,37 @@ export const TYPE = {
> Since 1.0.0-beta.3 we use the symbol itself for indexing the dependencies.
> Prior to this version we indexed the dependencies by the string of the symbol.
## Type-Safe Token (2.0 beta)
With version 2 we added the possibility to use a type-safe way to identify our dependencies. This is done with tokens:
```ts
export TYPE = {
"Service" = token<MyServiceInterface>("Service"),
// [...]
}
```
In this case the type `MyServiceInterface` is inherited when using `container.get(TYPE.Service)`, `resolve(TYPE.Service)`
and `wire(this, "service", TYPE.Service)`and does not need to be explicitly added. In case of the decorator `@inject(TYPE.Service)` it needs to be added
but it throws a type error if the types don't match:
```ts
class Example {
@inject(TYPE.Service) // throws a type error because WrongInterface is not compatible with MyServiceInterface
readonly service!: WrongInterface;
}
```

Correkt:

```ts
class Example {
@inject(TYPE.Service)
readonly service!: MyServiceInterface;
}
```

## Usage

#### Step 1 - Installing the OWJA! IoC library
Expand Down Expand Up @@ -429,4 +460,4 @@ but has other goals:

**MIT**

Copyright © 2019 Hauke Broer
Copyright © 2019-2022 The OWJA! Team
5 changes: 4 additions & 1 deletion src/example/service/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import {token} from "../../ioc/token";
import {MyOtherService} from "./my-other-service";

export const TYPE = {
MyService: Symbol("MyService"),
MyOtherService: Symbol("MyOtherService"),
MyOtherService: token<MyOtherService>("MyOtherService"),
};
7 changes: 6 additions & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Container, createDecorator, createResolve, createWire, NOCACHE} from "./";
import {Container, token, createDecorator, createResolve, createWire, NOCACHE} from "./";
import {Container as ContainerOriginal} from "./ioc/container";
import {token as tokenOriginal} from "./ioc/token";
import {createDecorator as createDecoratorOriginal} from "./ioc/decorator";
import {createWire as createWireOriginal} from "./ioc/wire";
import {createResolve as createResolveOriginal} from "./ioc/resolve";
Expand All @@ -10,6 +11,10 @@ describe("Module", () => {
expect(Container).toBe(ContainerOriginal);
});

test('should export "token" function', () => {
expect(token).toBe(tokenOriginal);
});

test('should export "createDecorator" function', () => {
expect(createDecorator).toBe(createDecoratorOriginal);
});
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export {createDecorator} from "./ioc/decorator";
export {createWire} from "./ioc/wire";
export {createResolve} from "./ioc/resolve";
export {NOCACHE} from "./ioc/symbol";
export {token} from "./ioc/token";
47 changes: 46 additions & 1 deletion src/ioc/container.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {Container} from "./container";
import {token} from "./token";

describe("Container", () => {
describe("Container using symbols", () => {
let container: Container;

const exampleSymbol = Symbol.for("example");
const stringToken = token<string>("exampleStr");

beforeEach(() => {
container = new Container();
Expand All @@ -16,6 +18,12 @@ describe("Container", () => {
expect(container.get<string>(exampleSymbol)).toBe("hello world 1");
expect(container.get<string>(exampleSymbol)).toBe("hello world 2");
expect(container.get<string>(exampleSymbol)).toBe("hello world 3");

container.bind(stringToken).toFactory(() => `hello world ${count++}`);

expect(container.get(stringToken)).toBe("hello world 4");
expect(container.get(stringToken)).toBe("hello world 5");
expect(container.get(stringToken)).toBe("hello world 6");
});

test("can bind a factory in singleton scope", () => {
Expand All @@ -28,6 +36,16 @@ describe("Container", () => {
expect(container.get<string>(exampleSymbol)).toBe("hello world 1");
expect(container.get<string>(exampleSymbol)).toBe("hello world 1");
expect(container.get<string>(exampleSymbol)).toBe("hello world 1");

count = 1;
container
.bind(stringToken)
.toFactory(() => `hello world ${count++}`)
.inSingletonScope();

expect(container.get(stringToken)).toBe("hello world 1");
expect(container.get(stringToken)).toBe("hello world 1");
expect(container.get(stringToken)).toBe("hello world 1");
});

test("should use cached data in singleton scope", () => {
Expand Down Expand Up @@ -60,6 +78,21 @@ describe("Container", () => {
expect(container.get<IExampleConstructable>(exampleSymbol).hello()).toBe("world 1");
expect(container.get<IExampleConstructable>(exampleSymbol).hello()).toBe("world 1");
expect(container.get<IExampleConstructable>(exampleSymbol).hello()).toBe("world 1");

const exampleToken = token<IExampleConstructable>("example");

container.bind<IExampleConstructable>(exampleToken).to(
class implements IExampleConstructable {
count = 1;
hello() {
return `world ${this.count++}`;
}
},
);

expect(container.get(exampleToken).hello()).toBe("world 1");
expect(container.get(exampleToken).hello()).toBe("world 1");
expect(container.get(exampleToken).hello()).toBe("world 1");
});

test("can bind a constructable in singleton scope", () => {
Expand All @@ -86,11 +119,18 @@ describe("Container", () => {
test("can bind a constant value", () => {
container.bind<string>(exampleSymbol).toValue("constant world");
expect(container.get<string>(exampleSymbol)).toBe("constant world");

container.bind(stringToken).toValue("constant world");
expect(container.get(stringToken)).toBe("constant world");
});

test("can bind a constant value of zero", () => {
container.bind<number>(exampleSymbol).toValue(0);
expect(container.get<string>(exampleSymbol)).toBe(0);

const numToken = token<number>("number");
container.bind(numToken).toValue(0);
expect(container.get(numToken)).toBe(0);
});

test("can bind a negative constant value", () => {
Expand All @@ -114,6 +154,11 @@ describe("Container", () => {
expect(() => container.bind(exampleSymbol)).toThrow("object can only bound once: Symbol(example)");
});

test("can not bind to a token more than once", () => {
container.bind(stringToken);
expect(() => container.bind(stringToken)).toThrow("object can only bound once: Token(Symbol(exampleStr))");
});

test("can not get unbound dependency", () => {
container.bind(exampleSymbol);
expect(() => container.get<string>(exampleSymbol)).toThrow("nothing is bound to Symbol(example)");
Expand Down
34 changes: 18 additions & 16 deletions src/ioc/container.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {getType, MaybeToken, stringifyToken} from "./token";

interface IConfig<T> {
object?: INewAble<T>;
factory?: Factory<T>;
Expand Down Expand Up @@ -48,29 +50,29 @@ export class Container {
private _registry: Registry = new Map<symbol, IConfig<any>>();
private _snapshots: Registry[] = [];

bind<T = never>(type: symbol): Bind<T> {
return new Bind<T>(this._add<T>(type));
bind<T = never>(token: MaybeToken<T>): Bind<T> {
return new Bind<T>(this._add<T>(token));
}

rebind<T = never>(type: symbol): Bind<T> {
return this.remove(type).bind<T>(type);
rebind<T = never>(token: MaybeToken<T>): Bind<T> {
return this.remove(token).bind<T>(token);
}

remove(type: symbol): Container {
if (this._registry.get(type) === undefined) {
throw `${type.toString()} was never bound`;
remove(token: MaybeToken): Container {
if (this._registry.get(getType(token)) === undefined) {
throw `${stringifyToken(token)} was never bound`;
}

this._registry.delete(type);
this._registry.delete(getType(token));

return this;
}

get<T = never>(type: symbol): T {
const regItem = this._registry.get(type);
get<T = never>(token: MaybeToken<T>): T {
const regItem = this._registry.get(getType(token));

if (regItem === undefined) {
throw `nothing bound to ${type.toString()}`;
throw `nothing bound to ${stringifyToken(token)}`;
}

const {object, factory, value, cache, singleton} = regItem;
Expand All @@ -86,7 +88,7 @@ export class Container {
if (typeof object !== "undefined") return cacheItem(() => new object());
if (typeof factory !== "undefined") return cacheItem(() => factory());

throw `nothing is bound to ${type.toString()}`;
throw `nothing is bound to ${stringifyToken(token)}`;
}

snapshot(): Container {
Expand All @@ -99,13 +101,13 @@ export class Container {
return this;
}

private _add<T>(type: symbol): IConfig<T> {
if (this._registry.get(type) !== undefined) {
throw `object can only bound once: ${type.toString()}`;
private _add<T>(token: MaybeToken<T>): IConfig<T> {
if (this._registry.get(getType(token)) !== undefined) {
throw `object can only bound once: ${stringifyToken(token)}`;
}

const conf = {singleton: false};
this._registry.set(type, conf);
this._registry.set(getType(token), conf);

return conf;
}
Expand Down
7 changes: 4 additions & 3 deletions src/ioc/decorator.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {Container} from "./container";
import {define} from "./define";
import {MaybeToken} from "./token";

export function createDecorator(container: Container) {
return (type: symbol, ...args: symbol[]) => {
return <T>(target: T, property: keyof T): void => {
define(target, property, container, type, args);
return <T>(token: MaybeToken<T>, ...args: MaybeToken[]) => {
return <TTarget extends {[key in TProp]: T}, TProp extends string>(target: TTarget, property: TProp): void => {
define(target, property, container, token, args);
};
};
}
13 changes: 10 additions & 3 deletions src/ioc/define.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import {Container} from "./container";
import {NOCACHE} from "./symbol";
import {MaybeToken} from "./token";

export function define<T>(target: T, property: keyof T, container: Container, type: symbol, args: symbol[]) {
export function define<TVal, TTarget extends {[key in TProp]: TVal}, TProp extends string>(
target: TTarget,
property: TProp,
container: Container,
token: MaybeToken<TVal>,
argTokens: MaybeToken[],
) {
Object.defineProperty(target, property, {
get: function () {
const value = container.get<any>(type);
if (args.indexOf(NOCACHE) === -1) {
const value = container.get<any>(token);
if (argTokens.indexOf(NOCACHE) === -1) {
Object.defineProperty(this, property, {
value,
enumerable: true,
Expand Down
5 changes: 3 additions & 2 deletions src/ioc/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {Container} from "./container";
import {NOCACHE} from "./symbol";
import {MaybeToken} from "./token";

export function createResolve(container: Container) {
return <T = unknown>(type: symbol, ...args: symbol[]) => {
return <T = never>(token: MaybeToken<T>, ...args: MaybeToken[]) => {
let value: T;
return (): T => {
if (args.indexOf(NOCACHE) !== -1 || value === undefined) {
value = container.get<T>(type);
value = container.get<T>(token);
}
return value;
};
Expand Down
32 changes: 32 additions & 0 deletions src/ioc/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export function token<T>(name: string) {
return {type: Symbol(name)} as Token<T>;
}

declare const typeMarker: unique symbol;

export interface Token<T> {
type: symbol;
[typeMarker]: T;
}

export type MaybeToken<T = unknown> = Token<T> | symbol;

function isToken<T>(token: MaybeToken<T>): token is Token<T> {
return typeof token != "symbol";
}

export function stringifyToken(token: MaybeToken): string {
if (isToken(token)) {
return `Token(${token.type.toString()})`;
} else {
return token.toString();
}
}

export function getType(token: MaybeToken): symbol {
if (isToken(token)) {
return token.type;
} else {
return token;
}
}
10 changes: 8 additions & 2 deletions src/ioc/wire.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import {Container} from "./container";
import {define} from "./define";
import {MaybeToken} from "./token";

export function createWire(container: Container) {
return <T>(target: T, property: keyof T, type: symbol, ...args: symbol[]) => {
define(target, property, container, type, args);
return <TVal, TTarget extends {[key in TProp]: TVal}, TProp extends string>(
target: TTarget,
property: TProp,
token: MaybeToken<TVal>,
...args: MaybeToken[]
) => {
define(target, property, container, token, args);
};
}

0 comments on commit a39b75f

Please sign in to comment.