Last active
April 7, 2024 20:40
-
-
Save temoncher/dfa24997f94c644d4246cde84bd299a2 to your computer and use it in GitHub Desktop.
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
type NeverIfNotExtends<T, P> = T extends P ? T : never; | |
type IFactoryBindConfig = { | |
fresh: true; | |
eager?: false; | |
} | { | |
fresh: false; | |
eager?: boolean; | |
} | |
interface IInitializer<T> { | |
get(): T; | |
} | |
class InstanceInitializer<T> implements IInitializer<T> { | |
constructor(private readonly instance: T) { } | |
get(): T { | |
return this.instance; | |
} | |
} | |
class OptionalInitializer implements IInitializer<null> { | |
constructor() { } | |
get(): null { | |
return null; | |
} | |
} | |
class FactoryInitializer<I extends Safeject, T> implements IInitializer<T> { | |
#initialized: T | null = null; | |
constructor( | |
private readonly di: I, | |
private readonly factory: (di: I) => T, | |
private readonly config: IFactoryBindConfig, | |
) { | |
if (config.eager) this.#initialized = factory(di); | |
} | |
get(): T { | |
if (this.config.fresh) { | |
return this.factory(this.di); | |
} | |
return this.#initialized ??= this.factory(this.di); | |
} | |
} | |
interface IAsyncInitializer<T> { | |
get(): Promise<T> | T; | |
} | |
class AsyncFactoryInitializer<I extends Safeject, T> implements IAsyncInitializer<T> { | |
#initialized: T | null = null; | |
#promise: Promise<T> | null = null; | |
constructor( | |
private readonly di: I, | |
private readonly factory: (container: I) => Promise<T>, | |
private readonly config: IFactoryBindConfig, | |
) { | |
if (config.eager) this.init(); | |
} | |
private init() { | |
return this.#promise ??= this.factory(this.di).then((res) => this.#initialized = res); | |
} | |
get(): Promise<T> | T { | |
if (this.config.fresh) { | |
return this.factory(this.di); | |
} | |
return this.#initialized ?? this.init(); | |
} | |
} | |
type GetTokenType<TJ extends Safeject<any, any, any>, TK> = TK extends SafejectToken<infer TKType> | |
? TK extends Safeject.RequiredOf<TJ> | |
? TKType | |
: TK extends Safeject.AsyncOf<TJ> | |
? Promise<TKType> | |
: TK extends Safeject.OptionalOf<TJ> | |
? TKType | null | |
: never | |
: never; | |
class Safeject< | |
in Services extends SafejectToken<any> = never, | |
in OptionalServices extends SafejectToken<any> = never, | |
in AsyncServices extends SafejectToken<any> = never | |
> implements Safeject<Services, OptionalServices, AsyncServices> { | |
static Token<Id extends string>(id: Id) { | |
return function <T>() { | |
return (class SafejectToken<in out T> { id = id }) | |
}; | |
}; | |
static #defaultConfig = { | |
eager: false, | |
fresh: false, | |
} as const satisfies IFactoryBindConfig; | |
private initializers: Map<SafejectToken<any>, IInitializer<any>>; | |
constructor(initializers?: Map<SafejectToken<any>, IInitializer<any>>) { | |
this.initializers = initializers ?? new Map<SafejectToken<any>, IInitializer<any>>(); | |
} | |
get<T extends Services>(token: T): SafejectToken.Type<T>; | |
get<T extends AsyncServices>(token: T): Promise<SafejectToken.Type<T>>; | |
get<T extends OptionalServices>(token: T): SafejectToken.Type<T> | null; | |
get<T extends Record<string, Safeject.AllOf<typeof this>>>(tokens: T): { [K in keyof T]: GetTokenType<typeof this, T[K]> }; | |
get<T extends Safeject.AllOf<typeof this> | Record<string, Safeject.AllOf<typeof this>>>(token: T): any { | |
if (token instanceof SafejectToken) { | |
const initializer = this.initializers.get(token); | |
if (!initializer) throw new Error(`Token ${token.id} not found`); | |
return initializer.get() as any; | |
} | |
const result = {}; | |
for (const key in token) { | |
if (Object.prototype.hasOwnProperty.call(token, key)) { | |
(result as any)[key] = this.get(token[key] as any); | |
} | |
} | |
return result; | |
} | |
unbind<T extends Safeject.AllOf<typeof this>>(token: T): Safeject<Exclude<Services, T>, Exclude<OptionalServices, T>, Exclude<AsyncServices, T>> { | |
this.initializers.delete(token); | |
return this as any; | |
} | |
bindOptional<T extends SafejectToken<any>>(token: T): Safeject<Exclude<Services, T>, OptionalServices | T, Exclude<AsyncServices, T>> { | |
this.initializers.set(token, new OptionalInitializer()); | |
return this as any; | |
}; | |
bindInstance<T extends SafejectToken<any>>(token: T, implementation: SafejectToken.Type<T>): Safeject<Services | T, OptionalServices | T, Exclude<AsyncServices, T>> { | |
this.initializers.set(token, new InstanceInitializer(implementation)); | |
return this as any; | |
} | |
bindClass<T extends SafejectToken<any>>( | |
token: T, | |
ServiceClass: new (di: Safeject<Services, OptionalServices, AsyncServices>) => SafejectToken.Type<T>, | |
config?: IFactoryBindConfig, | |
): Safeject<Services | T, OptionalServices | T, Exclude<AsyncServices, T>> { | |
const defaultedConfig = { | |
...Safeject.#defaultConfig, | |
...config, | |
}; | |
this.initializers.set(token, new FactoryInitializer(this, (di) => new ServiceClass(di), defaultedConfig)); | |
return this as any; | |
} | |
bindFactory<T extends SafejectToken<any>>( | |
token: T, | |
factory: (di: Safeject<Services, OptionalServices, AsyncServices>) => SafejectToken.Type<T>, | |
config?: IFactoryBindConfig, | |
): Safeject<Services | T, OptionalServices | T, Exclude<AsyncServices, T>> { | |
const defaultedConfig = { | |
...Safeject.#defaultConfig, | |
...config, | |
}; | |
this.initializers.set(token, new FactoryInitializer(this, factory, defaultedConfig)); | |
return this as any; | |
} | |
bindAsyncFactory<T extends SafejectToken<any>>( | |
token: T, | |
factory: (di: Safeject<Services, OptionalServices, AsyncServices>) => Promise<SafejectToken.Type<T>>, | |
config?: IFactoryBindConfig, | |
): Safeject<Services, OptionalServices, AsyncServices | T> { | |
const defaultedConfig = { | |
...Safeject.#defaultConfig, | |
...config, | |
}; | |
this.initializers.set(token, new AsyncFactoryInitializer(this, factory, defaultedConfig)); | |
return this as any; | |
} | |
async ensureLoaded<T extends AsyncServices[]>(...tokens: T): Promise<Safeject<Services | T[number], OptionalServices | T[number], Exclude<AsyncServices, T[number]>>> { | |
await Promise.all(tokens.map((t) => this.get(t as any))); | |
return this as any; | |
} | |
clone(): typeof this { | |
// TODO: clone initializers | |
return new Safeject(new Map(this.initializers)) as any; | |
} | |
} | |
declare namespace Safeject { | |
export type RequiredOf<T> = T extends Safeject<infer R> ? R : never; | |
export type OptionalOf<T> = T extends Safeject<any, infer O> ? O : never; | |
export type AsyncOf<T> = T extends Safeject<any, any, infer A> ? A : never; | |
export type AllOf<T> = T extends Safeject<infer R, infer O, infer A> ? R | O | A : never; | |
} | |
class SafejectToken<in out T> { | |
constructor(readonly id: string) { } | |
}; | |
declare namespace SafejectToken { | |
export type Type<T extends SafejectToken<any>> = T extends SafejectToken<infer S> ? S : never; | |
} | |
interface ILogger { | |
log(str: string): void; | |
} | |
const TLogger = new SafejectToken<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 SafejectToken<CustomAsyncLogger>("TAsyncLogger"); | |
type TAsyncLogger = typeof TAsyncLogger; | |
class CustomAsyncLogger { | |
logger = this.di.get(TLogger); | |
constructor(readonly di: Safeject<never, TLogger>) { } | |
async logAndWait() { | |
this.logger?.log("starting"); | |
await sleep(5000); | |
this.logger?.log("finished") | |
} | |
} | |
const TSomeService = new SafejectToken<SomeService>("TSomeService"); | |
type TSomeService = typeof TSomeService; | |
class SomeService { | |
private logger = this.di.get(TLogger); | |
private awaitLogger = this.di.get(TAsyncLogger); | |
constructor(private di: Safeject<TAsyncLogger, TLogger>) { } | |
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 SafejectToken<ISomeOtherService>("TSomeOtherService"); | |
type TSomeOtherService = typeof TSomeOtherService; | |
class SomeOtherService implements SafejectToken.Type<TSomeOtherService> { | |
private logger = this.di.get(TLogger); | |
private awaitLogger = this.di.get(TAsyncLogger); | |
constructor(private di: Safeject<TLogger | 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 SafejectToken<() => number>("TSomeFunction"); | |
type TSomeFunction = typeof TSomeFunction; | |
enum Env { | |
DEV = "DEV", | |
PROD = "PROD" | |
} | |
const TEnv = new SafejectToken<Env>("TEnv"); | |
type TEnv = typeof TEnv; | |
async function main() { | |
const envLoggerDi = new Safeject() | |
.bindInstance(TEnv, Env.DEV) | |
.bindInstance(TLogger, new ConsoleLogger()); | |
const di = envLoggerDi | |
.clone() | |
.unbind(TLogger) | |
.bindOptional(TLogger) | |
.bindClass(TAsyncLogger, CustomAsyncLogger) | |
.bindFactory(TSomeService, (di) => new SomeService(di)) | |
.bindAsyncFactory(TSomeFunction, () => Promise.resolve(() => 42)) | |
.bindAsyncFactory(TSomeOtherService, async (di) => { | |
await sleep(3000); | |
return new SomeOtherService(di); | |
}); | |
const kek = di.get({ logger: TLogger, env: TEnv }); | |
console.log("kek.logger", kek.logger) | |
console.log("kek.env", kek.env) | |
const asyncLogger = di.get(TAsyncLogger); | |
asyncLogger.logAndWait(); | |
type a1 = Safeject.AsyncOf<typeof di>; | |
const someOtherService1 = await di.get(TSomeOtherService); | |
await someOtherService1.otherRun(); | |
const awaitedDi = await di.ensureLoaded(TSomeOtherService, TSomeFunction); | |
type a = Safeject.AsyncOf<typeof awaitedDi>; | |
const someOtherService = awaitedDi.get(TSomeOtherService); | |
console.log(someOtherService); | |
function foo(di: Safeject<never, TLogger>) { | |
const logger = di.get(TLogger); | |
logger?.log("eggege"); | |
} | |
foo(di); | |
} | |
main(); | |
abstract class Provider<Token extends SafejectToken<any>> { | |
constructor(readonly token: Token) { } | |
abstract get(newDi: Inj): SafejectToken.Type<Token> | null | Promise<SafejectToken.Type<Token>>; | |
abstract clone(): Provider<Token>; | |
} | |
class OptionalProvider<Token extends SafejectToken<any>> extends Provider<Token> { | |
override get(di: Inj): SafejectToken.Type<Token> | null { | |
return null; | |
} | |
override clone() { | |
return new OptionalProvider(this.token); | |
} | |
} | |
class ValueProvider<Token extends SafejectToken<any>> extends OptionalProvider<Token> { | |
constructor(readonly token: Token, readonly value: SafejectToken.Type<Token>) { | |
super(token) | |
} | |
override get(di: Inj): SafejectToken.Type<Token> { | |
return this.value; | |
} | |
override clone() { | |
return new ValueProvider(this.token, this.value); | |
} | |
} | |
class FactoryProvider<Token extends SafejectToken<any>, DI extends Inj> extends ValueProvider<Token> { | |
constructor(readonly token: Token, readonly factory: (di: DI) => SafejectToken.Type<Token>) { | |
super(token, null as any) | |
} | |
override get(di: DI): SafejectToken.Type<Token> { | |
return this.factory(di); | |
} | |
override clone() { | |
return new FactoryProvider(this.token, this.factory); | |
} | |
} | |
class LazyValueProvider<Token extends SafejectToken<any>, DI extends Inj> extends ValueProvider<Token> { | |
instance: SafejectToken.Type<Token> | null = null; | |
constructor(readonly token: Token, readonly factory: (di: DI) => SafejectToken.Type<Token>) { | |
super(token, null as any); | |
} | |
override get(di: DI): SafejectToken.Type<Token> { | |
return this.instance ??= this.factory(di); | |
} | |
override clone() { | |
return new LazyValueProvider(this.token, this.factory); | |
} | |
} | |
type ExtractValue<Providers, Token> = Token extends SafejectToken<infer V> | |
? ValueProvider<Token> extends Providers | |
? OptionalProvider<Token> extends Providers | |
? V | null | |
: V | |
: Promise<V> | |
: never | |
type TokensFromProv<Providers> = Providers extends Provider<infer T> ? T : never; | |
class Inj<Providers extends Provider<any> = never> { | |
map = new Map<SafejectToken<any>, Provider<any>>(); | |
unbind<T extends TokensFromProv<Providers>>(token: T): Inj<Exclude<Providers, Provider<T>>> { | |
this.map.delete(token); | |
return this as any; | |
} | |
bind<T extends SafejectToken<any>, P extends Provider<T>>(provider: P): Inj<Providers | P> { | |
this.map.set(provider.token, provider); | |
return this as any; | |
}; | |
get<Token extends TokensFromProv<Providers>>(token: Token): ExtractValue<Providers, Token> { | |
const provider = this.map.get(token); | |
return provider?.get(this as any) as any; | |
} | |
clone() { | |
const newInj = new Inj<any>(); | |
for (const provider of this.map.values()) { | |
newInj.bind(provider.clone()) | |
} | |
return newInj; | |
} | |
} | |
async function ensureLoaded<DI extends Inj<any>, Tokens extends SafejectToken<any>[]>(di: DI, ...tokens: Tokens) { | |
await Promise.all(tokens.map((t) => di.get(t))); | |
return di as any; | |
} | |
type Optional<T extends SafejectToken<any>> = T & { __optional: true }; | |
type Async<T extends SafejectToken<any>> = T & { __async: true }; | |
type Multi<T extends SafejectToken<any>> = T & { __multi: true }; | |
type AsynOptionalMultiLogger = Async<Optional<Multi<TLogger>>>; | |
type o = OptionalProvider<TLogger> extends Provider<TLogger> ? true : false; | |
type v = ValueProvider<TLogger> extends Provider<TLogger> ? true : false; | |
type qq = OptionalProvider<TLogger> extends ValueProvider<TLogger> ? true : false; | |
type qqq = ValueProvider<TLogger> extends OptionalProvider<TLogger> ? true : false; | |
type qqqq = ValueProvider<TLogger> extends (OptionalProvider<TLogger> | ValueProvider<TAsyncLogger>) ? true : false; | |
async function main2() { | |
const di = new Inj() | |
.bind(new ValueProvider(TLogger, new ConsoleLogger())) | |
.bind(new LazyValueProvider(TAsyncLogger, (di) => new CustomAsyncLogger(di))); | |
const rre = di.get(TLogger); | |
const rree = di.get(TSomeOtherService); | |
function foo(di: Inj<OptionalProvider<TLogger>>) { | |
} | |
foo(di); | |
} | |
function value<T>(val: T) { | |
return { | |
__tag: 'ValueProvider', | |
value: val | |
} as const | |
} | |
function factory<const T extends (() => any)>(fac: T) { | |
return { | |
__tag: 'FactoryProvider', | |
factory: fac | |
} as const | |
} | |
class Test<S extends string> { | |
constructor(readonly innerData: S) {} | |
infer<const P extends { __tag: 'FactoryProvider'; factory: (innerData: S) => T }, T>(provider: P): T; | |
infer<const P extends { __tag: 'ValueProvider'; value: T }, T>(provider: P): T; | |
infer<const P extends { __tag: string }>(provider: P): any { | |
return provider; | |
} | |
} | |
const test = new Test("LOL"); | |
const qqq = test.infer(value(42)); | |
const qq = test.infer(factory((kek) => 42)); | |
const qq2 = test.infer({ | |
__tag: 'FactoryProvider', | |
factory: (tt) => tt, | |
}); | |
const q = test.infer(new FactoryProvider(undefined as any, () => 42)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment