Skip to content

Commit

Permalink
Add Token to help with typings
Browse files Browse the repository at this point in the history
  • Loading branch information
smonn committed Apr 1, 2022
1 parent dc53d56 commit fb89eba
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 98 deletions.
66 changes: 39 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,55 +17,67 @@ yarn add @smonn/container
Basic example usage. See more examples in the test file.

```ts
import { Container } from '@smonn/container';
import { Container, createToken } from '@smonn/container';

class Greeter {
// Note that this tag does not need to live with the class.
// Also note that the 'Greeter' part is optional due to how symbols work,
// but it's recommended to include it to help debug misconfigurations.
static tag: Symbol('Greeter');

sayHello(name: string): string {
return `Hello, ${name}!`;
}
// Interfaces are optional, but can help to ensure you depend on abstractions only.
interface Greeter {
sayHello(): string;
}
interface Shouter {
shoutHello(): string;
}

// It's recommended to assemble all tokens in one place for easier management.
// Use the createToken utility to assist with typings.
const Tokens = {
greeter: createToken<Greeter>('greeter'),
shouter: createToken<Shouter>('shouter'),
name: createToken<string>('name'),
} as const;

class Shouter {
static tag: Symbol('Shouter');
class Greeter implements Greeter {
constructor(private readonly name: string) {}

sayHello(): string {
return `Hello, ${this.name}!`;
}
}
class Shouter implements Shouter {
constructor(private readonly greeter: Greeter) {}

shout(name: string): string {
return this.greeter.sayHello(name).toUpperCase();
shoutHello(): string {
return this.greeter.sayHello().toUpperCase();
}
}

// Group together related classes in a single function to avoid a single
// massive function defining hundreds of dependencies. Also note that thanks to
// the token spec, explicitly declaring the generic type is not required.
function provideModule(container: Container) {
// Group together related classes in a single function to avoid a single
// massive function defining hundreds of dependencies.
container.set(Greeter.tag, () => new Greeter());
container.set(Shouter.tag, (c) => new Shouter(c.get<Greeter>(Greeter.tag)));
// Literal/basic values are allowed
container.set(Tokens.name, () => 'Joy');
container.set(Tokens.greeter, (c) => new Greeter(c.get(Tokens.name)));
container.set(Tokens.shouter, (c) => new Shouter(c.get(Tokens.greeter)));
}

const container = new Container();
container.register(provideModule)
container.register(provideModule);

const shouter = container.get<Shouter>(Shouter.tag);
console.log(shouter.shout('Joy')); // logs 'HELLO, JOY!'
// Here shouter will have the correct type (the Shouter interface)
const shouter = container.get(Tokens.shouter);
console.log(shouter.shoutHello()); // logs 'HELLO, JOY!'

console.log(
'always get the same instance',
Object.is(
container.get<Shouter>(Shouter.tag),
container.get<Shouter>(Shouter.tag)
) === true
Object.is(container.get(Tokens.shouter), container.get(Tokens.shouter)) ===
true
);

console.log(
'always get a new instance',
Object.is(
container.create<Shouter>(Shouter.tag),
container.create<Shouter>(Shouter.tag)
container.create(Tokens.shouter),
container.create(Tokens.shouter)
) === false
);
```
95 changes: 72 additions & 23 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,61 @@ export type Factory<T = unknown> = (container: Container) => T;
*/
export type Provider = (container: Container) => void;

/**
* Used to identify the type of a factory.
*/
export interface Token<T = unknown> {
/**
* Name of the token.
*/
name: string;

/**
* Type mapping for TypeScript to circumvent unused variable error.
* Do not access this property directly, it is always undefined.
* @ignore
* @private
*/
type: T;
}

/**
* Private implementation of the `Token` interface.
* @private
* @ignore
*/
class TokenImpl<T = unknown> implements Token<T> {
#name: string;
#type: T;

constructor(name: string) {
this.#name = name;
this.#type = void 0 as unknown as T;
}

get name() {
return this.#name;
}

get type() {
return this.#type;
}
}

/**
* Helper function to create a token.
* @param name Name of the token.
*/
export function createToken<T = unknown>(name: string): Token<T> {
return new TokenImpl<T>(name);
}

/**
* The container class.
*/
export class Container {
#factories: Map<symbol, Factory> = new Map();
#instances: Map<symbol, unknown> = new Map();
#factories = new Map<Token, Factory>();
#instances = new Map<Token, unknown>();

/**
* Get the number of factories registered.
Expand All @@ -25,13 +74,13 @@ export class Container {

/**
* Set a factory for a key.
* @param key Unique identifier for the factory.
* @param token Unique identifier for the factory.
* @param factory Factory function that returns a value.
* @returns
*/
set<T>(key: symbol, factory: Factory<T>): Container {
this.#instances.delete(key);
this.#factories.set(key, factory);
set<T, F extends T>(token: Token<T>, factory: Factory<F>): Container {
this.#instances.delete(token);
this.#factories.set(token, factory);
return this;
}

Expand All @@ -47,51 +96,51 @@ export class Container {

/**
* Verifies if a factory has been registered for a given key.
* @param key Unique identifier for the instance.
* @param token Unique identifier for the instance.
* @returns
*/
has(key: symbol): boolean {
return this.#factories.has(key);
has<T>(token: Token<T>): boolean {
return this.#factories.has(token);
}

/**
* Get an instance of a key. Always returns the same instance.
* Will create a new instance if one does not exist.
* @param key Unique identifier for the instance.
* @param token Unique identifier for the instance.
* @returns
*/
get<T>(key: symbol): T {
if (this.#instances.has(key)) {
return this.#instances.get(key) as T;
get<T>(token: Token<T>): T {
if (this.#instances.has(token)) {
return this.#instances.get(token) as T;
}

const instance = this.create<T>(key);
this.#instances.set(key, instance);
const instance = this.create(token);
this.#instances.set(token, instance);
return instance;
}

/**
* Get a instance of a key. Always returns a new instance.
* @param key Unique identifier for the instance.
* @param token Unique identifier for the instance.
* @returns
*/
create<T>(key: symbol): T {
const factory = this.#factories.get(key);
create<T>(token: Token<T>): T {
const factory = this.#factories.get(token);

if (!factory)
throw new Error(`No factory registered for key ${String(key)}`);
throw new Error(`No factory registered for key ${String(token)}`);

const instance = factory(this);
return instance as T;
}

/**
* Delete a factory for a key. Also deletes the instance if it exists.
* @param key Unique identifier for the instance.
* @param token Unique identifier for the instance.
*/
delete(key: symbol): boolean {
this.#instances.delete(key);
return this.#factories.delete(key);
delete<T>(token: Token<T>): boolean {
this.#instances.delete(token);
return this.#factories.delete(token);
}

/**
Expand Down
63 changes: 63 additions & 0 deletions src/readme-sample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Container, createToken } from './index';

// Interfaces are optional, but can help to ensure you depend on abstractions only.
interface Greeter {
sayHello(): string;
}
interface Shouter {
shoutHello(): string;
}

// It's recommended to assemble all tokens in one place for easier management.
// Use the createToken utility to assist with typings.
const Tokens = {
greeter: createToken<Greeter>('greeter'),
shouter: createToken<Shouter>('shouter'),
name: createToken<string>('name'),
} as const;

class Greeter implements Greeter {
constructor(private readonly name: string) {}

sayHello(): string {
return `Hello, ${this.name}!`;
}
}
class Shouter implements Shouter {
constructor(private readonly greeter: Greeter) {}

shoutHello(): string {
return this.greeter.sayHello().toUpperCase();
}
}

// Group together related classes in a single function to avoid a single
// massive function defining hundreds of dependencies. Also note that thanks to
// the token spec, explicitly declaring the generic type is not required.
function provideModule(container: Container) {
// Literal/basic values are allowed
container.set(Tokens.name, () => 'Joy');
container.set(Tokens.greeter, (c) => new Greeter(c.get(Tokens.name)));
container.set(Tokens.shouter, (c) => new Shouter(c.get(Tokens.greeter)));
}

const container = new Container();
container.register(provideModule);

// Here shouter will have the correct type (the Shouter interface)
const shouter = container.get(Tokens.shouter);
console.log(shouter.shoutHello()); // logs 'HELLO, JOY!'

console.log(
'always get the same instance',
Object.is(container.get(Tokens.shouter), container.get(Tokens.shouter)) ===
true
);

console.log(
'always get a new instance',
Object.is(
container.create(Tokens.shouter),
container.create(Tokens.shouter)
) === false
);
Loading

0 comments on commit fb89eba

Please sign in to comment.