Last active
April 16, 2024 12:05
-
-
Save temoncher/fcdef88f0db3de2c619bcb8e4d4cc3db to your computer and use it in GitHub Desktop.
Minimal typed injector
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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