Skip to content

Instantly share code, notes, and snippets.

@temoncher
Last active April 16, 2024 12:05
Show Gist options
  • Save temoncher/fcdef88f0db3de2c619bcb8e4d4cc3db to your computer and use it in GitHub Desktop.
Save temoncher/fcdef88f0db3de2c619bcb8e4d4cc3db to your computer and use it in GitHub Desktop.
Minimal typed injector
import { mapValues } from 'remeda';
export class Token<in out T> {
constructor(readonly id: string) {}
}
export declare namespace Token {
export type Type<T extends Token<any>> = T extends Token<infer S> ? S : never;
}
export class Container<in TTokens extends Token<any> = never> {
constructor(
private readonly unsafeMap = new Map<
Token<any>,
(di: Container<any>) => any
>()
) {}
unbind<T extends TTokens>(token: T): Container<Exclude<TTokens, T>> {
const newMap = new Map(this.unsafeMap);
newMap.delete(token);
return new Container(newMap);
}
bind<T extends Token<any>>(
token: T,
factory: (di: Container<TTokens>) => Token.Type<T>
): Container<TTokens | T> {
const newMap = new Map(this.unsafeMap);
newMap.set(token, factory);
return new Container(newMap);
}
getOptional<T extends Token<any>>(token: T): Token.Type<T> | undefined;
getOptional<TRecord extends Record<string, Token<any>>>(
record: TRecord
): { [K in keyof TRecord]: Token.Type<TRecord[K]> | undefined };
getOptional(tokenOrRecord: Token<any> | Record<string, Token<any>>): any {
if (tokenOrRecord instanceof Token) {
const factory = this.unsafeMap.get(tokenOrRecord);
return factory ? factory(this) : undefined;
}
return mapValues(tokenOrRecord, (token) => this.getOptional(token));
}
get<T extends TTokens>(token: T): Token.Type<T>;
get<TRecord extends Record<string, TTokens>>(
record: TRecord
): {
[K in keyof TRecord]: Token.Type<TRecord[K]>;
};
get<T extends TTokens>(token: T): Token.Type<T> {
return this.getOptional(token)!;
}
clone(): Container<TTokens> {
return new Container(new Map(this.unsafeMap));
}
}
export declare namespace Container {
export type Tokens<C> = C extends Container<infer T> ? T : never;
}
export function singleton<TTokens extends Token<any>, TResult>(
lazy: (di: Container<TTokens>) => TResult
) {
let instance: TResult | undefined;
return (di: Container<TTokens>) => (instance ??= lazy(di));
}
// ----------------------------------
interface ILogger {
log(str: string): void;
}
const TLogger = new Token<ILogger>("TLogger");
type TLogger = typeof TLogger;
class ConsoleLogger implements ILogger {
log(str: string) {
console.log(str);
}
}
const sleep = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay))
const TAsyncLogger = new Token<CustomAsyncLogger>("TAsyncLogger");
type TAsyncLogger = typeof TAsyncLogger;
class CustomAsyncLogger {
logger = this.di.get(TLogger);
constructor(readonly di: Container<TLogger>) { }
async logAndWait() {
this.logger?.log("starting");
await sleep(5000);
this.logger?.log("finished")
}
}
const TSomeService = new Token<SomeService>("TSomeService");
type TSomeService = typeof TSomeService;
class SomeService {
private logger = this.di.getOptional(TLogger);
private awaitLogger = this.di.get(TAsyncLogger);
constructor(private di: Container<TAsyncLogger>) { }
async run() {
this.logger?.log("SomeService.run.Start");
await this.awaitLogger.logAndWait();
this.logger?.log("SomeService.run.Start");
}
}
interface ISomeOtherService {
otherRun(): Promise<void>;
}
const TSomeOtherService = new Token<ISomeOtherService>("TSomeOtherService");
type TSomeOtherService = typeof TSomeOtherService;
class SomeOtherService implements Token.Type<TSomeOtherService> {
private logger = this.di.getOptional(TLogger);
private awaitLogger = this.di.get(TAsyncLogger);
constructor(private di: Container<TAsyncLogger>) {
this.logger?.log(`${SomeOtherService.name}.constructor`);
}
async otherRun() {
this.logger?.log(`${SomeOtherService.name}.otherRun.Start`);
await this.awaitLogger.logAndWait();
this.logger?.log(`${SomeOtherService.name}.otherRun.End`);
}
}
const TSomeFunction = new Token<() => number>("TSomeFunction");
type TSomeFunction = typeof TSomeFunction;
enum Env {
DEV = "DEV",
PROD = "PROD"
}
const TEnv = new Token<Env>("TEnv");
type TEnv = typeof TEnv;
async function main2() {
const di = new Container()
// .bind(TLogger, value(new ConsoleLogger()))
.bind(TAsyncLogger, singleton((di) => new CustomAsyncLogger(di)))
.bind(TSomeOtherService, (di) => new SomeOtherService(di));
const rre = di.get(TLogger);
const rree = di.get(TSomeOtherService);
function foo(di: Container<TLogger>) {
}
foo(di);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment