Skip to content

Instantly share code, notes, and snippets.

@temoncher
Last active April 7, 2024 20:40
Show Gist options
  • Save temoncher/dfa24997f94c644d4246cde84bd299a2 to your computer and use it in GitHub Desktop.
Save temoncher/dfa24997f94c644d4246cde84bd299a2 to your computer and use it in GitHub Desktop.
Typed injector
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