Skip to content

Commit a39b75f

Browse files
author
Hauke B
authored
Merge pull request #51 from owja/token
Add support for type-safe token based injection Published as 2.0.0-alpha.2
2 parents 9750045 + 7320956 commit a39b75f

File tree

11 files changed

+164
-30
lines changed

11 files changed

+164
-30
lines changed

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,37 @@ export const TYPE = {
237237
> Since 1.0.0-beta.3 we use the symbol itself for indexing the dependencies.
238238
> Prior to this version we indexed the dependencies by the string of the symbol.
239239
240+
## Type-Safe Token (2.0 beta)
241+
242+
With version 2 we added the possibility to use a type-safe way to identify our dependencies. This is done with tokens:
243+
244+
```ts
245+
export TYPE = {
246+
"Service" = token<MyServiceInterface>("Service"),
247+
// [...]
248+
}
249+
```
250+
251+
In this case the type `MyServiceInterface` is inherited when using `container.get(TYPE.Service)`, `resolve(TYPE.Service)`
252+
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
253+
but it throws a type error if the types don't match:
254+
255+
```ts
256+
class Example {
257+
@inject(TYPE.Service) // throws a type error because WrongInterface is not compatible with MyServiceInterface
258+
readonly service!: WrongInterface;
259+
}
260+
```
261+
262+
Correkt:
263+
264+
```ts
265+
class Example {
266+
@inject(TYPE.Service)
267+
readonly service!: MyServiceInterface;
268+
}
269+
```
270+
240271
## Usage
241272

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

430461
**MIT**
431462

432-
Copyright © 2019 Hauke Broer
463+
Copyright © 2019-2022 The OWJA! Team

src/example/service/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import {token} from "../../ioc/token";
2+
import {MyOtherService} from "./my-other-service";
3+
14
export const TYPE = {
25
MyService: Symbol("MyService"),
3-
MyOtherService: Symbol("MyOtherService"),
6+
MyOtherService: token<MyOtherService>("MyOtherService"),
47
};

src/index.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import {Container, createDecorator, createResolve, createWire, NOCACHE} from "./";
1+
import {Container, token, createDecorator, createResolve, createWire, NOCACHE} from "./";
22
import {Container as ContainerOriginal} from "./ioc/container";
3+
import {token as tokenOriginal} from "./ioc/token";
34
import {createDecorator as createDecoratorOriginal} from "./ioc/decorator";
45
import {createWire as createWireOriginal} from "./ioc/wire";
56
import {createResolve as createResolveOriginal} from "./ioc/resolve";
@@ -10,6 +11,10 @@ describe("Module", () => {
1011
expect(Container).toBe(ContainerOriginal);
1112
});
1213

14+
test('should export "token" function', () => {
15+
expect(token).toBe(tokenOriginal);
16+
});
17+
1318
test('should export "createDecorator" function', () => {
1419
expect(createDecorator).toBe(createDecoratorOriginal);
1520
});

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export {createDecorator} from "./ioc/decorator";
33
export {createWire} from "./ioc/wire";
44
export {createResolve} from "./ioc/resolve";
55
export {NOCACHE} from "./ioc/symbol";
6+
export {token} from "./ioc/token";

src/ioc/container.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {Container} from "./container";
2+
import {token} from "./token";
23

3-
describe("Container", () => {
4+
describe("Container using symbols", () => {
45
let container: Container;
56

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

810
beforeEach(() => {
911
container = new Container();
@@ -16,6 +18,12 @@ describe("Container", () => {
1618
expect(container.get<string>(exampleSymbol)).toBe("hello world 1");
1719
expect(container.get<string>(exampleSymbol)).toBe("hello world 2");
1820
expect(container.get<string>(exampleSymbol)).toBe("hello world 3");
21+
22+
container.bind(stringToken).toFactory(() => `hello world ${count++}`);
23+
24+
expect(container.get(stringToken)).toBe("hello world 4");
25+
expect(container.get(stringToken)).toBe("hello world 5");
26+
expect(container.get(stringToken)).toBe("hello world 6");
1927
});
2028

2129
test("can bind a factory in singleton scope", () => {
@@ -28,6 +36,16 @@ describe("Container", () => {
2836
expect(container.get<string>(exampleSymbol)).toBe("hello world 1");
2937
expect(container.get<string>(exampleSymbol)).toBe("hello world 1");
3038
expect(container.get<string>(exampleSymbol)).toBe("hello world 1");
39+
40+
count = 1;
41+
container
42+
.bind(stringToken)
43+
.toFactory(() => `hello world ${count++}`)
44+
.inSingletonScope();
45+
46+
expect(container.get(stringToken)).toBe("hello world 1");
47+
expect(container.get(stringToken)).toBe("hello world 1");
48+
expect(container.get(stringToken)).toBe("hello world 1");
3149
});
3250

3351
test("should use cached data in singleton scope", () => {
@@ -60,6 +78,21 @@ describe("Container", () => {
6078
expect(container.get<IExampleConstructable>(exampleSymbol).hello()).toBe("world 1");
6179
expect(container.get<IExampleConstructable>(exampleSymbol).hello()).toBe("world 1");
6280
expect(container.get<IExampleConstructable>(exampleSymbol).hello()).toBe("world 1");
81+
82+
const exampleToken = token<IExampleConstructable>("example");
83+
84+
container.bind<IExampleConstructable>(exampleToken).to(
85+
class implements IExampleConstructable {
86+
count = 1;
87+
hello() {
88+
return `world ${this.count++}`;
89+
}
90+
},
91+
);
92+
93+
expect(container.get(exampleToken).hello()).toBe("world 1");
94+
expect(container.get(exampleToken).hello()).toBe("world 1");
95+
expect(container.get(exampleToken).hello()).toBe("world 1");
6396
});
6497

6598
test("can bind a constructable in singleton scope", () => {
@@ -86,11 +119,18 @@ describe("Container", () => {
86119
test("can bind a constant value", () => {
87120
container.bind<string>(exampleSymbol).toValue("constant world");
88121
expect(container.get<string>(exampleSymbol)).toBe("constant world");
122+
123+
container.bind(stringToken).toValue("constant world");
124+
expect(container.get(stringToken)).toBe("constant world");
89125
});
90126

91127
test("can bind a constant value of zero", () => {
92128
container.bind<number>(exampleSymbol).toValue(0);
93129
expect(container.get<string>(exampleSymbol)).toBe(0);
130+
131+
const numToken = token<number>("number");
132+
container.bind(numToken).toValue(0);
133+
expect(container.get(numToken)).toBe(0);
94134
});
95135

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

157+
test("can not bind to a token more than once", () => {
158+
container.bind(stringToken);
159+
expect(() => container.bind(stringToken)).toThrow("object can only bound once: Token(Symbol(exampleStr))");
160+
});
161+
117162
test("can not get unbound dependency", () => {
118163
container.bind(exampleSymbol);
119164
expect(() => container.get<string>(exampleSymbol)).toThrow("nothing is bound to Symbol(example)");

src/ioc/container.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {getType, MaybeToken, stringifyToken} from "./token";
2+
13
interface IConfig<T> {
24
object?: INewAble<T>;
35
factory?: Factory<T>;
@@ -48,29 +50,29 @@ export class Container {
4850
private _registry: Registry = new Map<symbol, IConfig<any>>();
4951
private _snapshots: Registry[] = [];
5052

51-
bind<T = never>(type: symbol): Bind<T> {
52-
return new Bind<T>(this._add<T>(type));
53+
bind<T = never>(token: MaybeToken<T>): Bind<T> {
54+
return new Bind<T>(this._add<T>(token));
5355
}
5456

55-
rebind<T = never>(type: symbol): Bind<T> {
56-
return this.remove(type).bind<T>(type);
57+
rebind<T = never>(token: MaybeToken<T>): Bind<T> {
58+
return this.remove(token).bind<T>(token);
5759
}
5860

59-
remove(type: symbol): Container {
60-
if (this._registry.get(type) === undefined) {
61-
throw `${type.toString()} was never bound`;
61+
remove(token: MaybeToken): Container {
62+
if (this._registry.get(getType(token)) === undefined) {
63+
throw `${stringifyToken(token)} was never bound`;
6264
}
6365

64-
this._registry.delete(type);
66+
this._registry.delete(getType(token));
6567

6668
return this;
6769
}
6870

69-
get<T = never>(type: symbol): T {
70-
const regItem = this._registry.get(type);
71+
get<T = never>(token: MaybeToken<T>): T {
72+
const regItem = this._registry.get(getType(token));
7173

7274
if (regItem === undefined) {
73-
throw `nothing bound to ${type.toString()}`;
75+
throw `nothing bound to ${stringifyToken(token)}`;
7476
}
7577

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

89-
throw `nothing is bound to ${type.toString()}`;
91+
throw `nothing is bound to ${stringifyToken(token)}`;
9092
}
9193

9294
snapshot(): Container {
@@ -99,13 +101,13 @@ export class Container {
99101
return this;
100102
}
101103

102-
private _add<T>(type: symbol): IConfig<T> {
103-
if (this._registry.get(type) !== undefined) {
104-
throw `object can only bound once: ${type.toString()}`;
104+
private _add<T>(token: MaybeToken<T>): IConfig<T> {
105+
if (this._registry.get(getType(token)) !== undefined) {
106+
throw `object can only bound once: ${stringifyToken(token)}`;
105107
}
106108

107109
const conf = {singleton: false};
108-
this._registry.set(type, conf);
110+
this._registry.set(getType(token), conf);
109111

110112
return conf;
111113
}

src/ioc/decorator.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {Container} from "./container";
22
import {define} from "./define";
3+
import {MaybeToken} from "./token";
34

45
export function createDecorator(container: Container) {
5-
return (type: symbol, ...args: symbol[]) => {
6-
return <T>(target: T, property: keyof T): void => {
7-
define(target, property, container, type, args);
6+
return <T>(token: MaybeToken<T>, ...args: MaybeToken[]) => {
7+
return <TTarget extends {[key in TProp]: T}, TProp extends string>(target: TTarget, property: TProp): void => {
8+
define(target, property, container, token, args);
89
};
910
};
1011
}

src/ioc/define.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import {Container} from "./container";
22
import {NOCACHE} from "./symbol";
3+
import {MaybeToken} from "./token";
34

4-
export function define<T>(target: T, property: keyof T, container: Container, type: symbol, args: symbol[]) {
5+
export function define<TVal, TTarget extends {[key in TProp]: TVal}, TProp extends string>(
6+
target: TTarget,
7+
property: TProp,
8+
container: Container,
9+
token: MaybeToken<TVal>,
10+
argTokens: MaybeToken[],
11+
) {
512
Object.defineProperty(target, property, {
613
get: function () {
7-
const value = container.get<any>(type);
8-
if (args.indexOf(NOCACHE) === -1) {
14+
const value = container.get<any>(token);
15+
if (argTokens.indexOf(NOCACHE) === -1) {
916
Object.defineProperty(this, property, {
1017
value,
1118
enumerable: true,

src/ioc/resolve.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import {Container} from "./container";
22
import {NOCACHE} from "./symbol";
3+
import {MaybeToken} from "./token";
34

45
export function createResolve(container: Container) {
5-
return <T = unknown>(type: symbol, ...args: symbol[]) => {
6+
return <T = never>(token: MaybeToken<T>, ...args: MaybeToken[]) => {
67
let value: T;
78
return (): T => {
89
if (args.indexOf(NOCACHE) !== -1 || value === undefined) {
9-
value = container.get<T>(type);
10+
value = container.get<T>(token);
1011
}
1112
return value;
1213
};

src/ioc/token.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export function token<T>(name: string) {
2+
return {type: Symbol(name)} as Token<T>;
3+
}
4+
5+
declare const typeMarker: unique symbol;
6+
7+
export interface Token<T> {
8+
type: symbol;
9+
[typeMarker]: T;
10+
}
11+
12+
export type MaybeToken<T = unknown> = Token<T> | symbol;
13+
14+
function isToken<T>(token: MaybeToken<T>): token is Token<T> {
15+
return typeof token != "symbol";
16+
}
17+
18+
export function stringifyToken(token: MaybeToken): string {
19+
if (isToken(token)) {
20+
return `Token(${token.type.toString()})`;
21+
} else {
22+
return token.toString();
23+
}
24+
}
25+
26+
export function getType(token: MaybeToken): symbol {
27+
if (isToken(token)) {
28+
return token.type;
29+
} else {
30+
return token;
31+
}
32+
}

0 commit comments

Comments
 (0)