На работе в проекте столкнулись с достаточно распространенной архитектурной проблемой. Есть набор сервисов, зависящих друг от друга. Первоначально взаимосвязи между сервисами описывались c помощью DI через конструктор.

interface Options {
	service1: Service1;
	service2: Service2;
	serviceN: ServiceN;
}
class ServiceNew {
	constructor(private options: Options) {}
}

В большинстве случаев такой подход является достаточно эффективным. Однако порой возникают ситуации, например, когда 2 сервиса зависят друг от друга и приходится инжектить зависимость через функции-геттеры.

class Sevice1 {
	constructor(
		private getService2(): Service2,
		private serviceN: ServiceN
	) {}
}
class Sevice2 {
	constructor(
		private getService1(): Service1,
		private serviceN: ServiceN
	) {}
}

В ситуации когда таких сервисов например 20, то хочется уже более единообразного подхода. Например мы можем обратиться к опыту языка Java и сохранить все сервисы в один объект и передать ссылку на него в каждый сервис. А для большей изоляции кода можно на уровне типов ограничить список используемых сервисов. Главное условие при таком подходе, что нельзя обращаться к такому объекту в конструкторе, тк в этот момент еще идет наполнение этого объекта. При этом остается декларативность в описании зависимостей и сохраняется единообразность подхода. Также если в каком-то сервисе мы вдруг забудем про объявлении поля ctx, то type-checker об этом напомнит.

interface IServiceCtx {
	service1: Service1;
	service2: Service2;
	// .....
	serviceN: ServiceN;
}

class Service1 {
	ctx!: Pick<IServiceCtx, 'service2' | 'serviceN'>
}
class Service2 {
	ctx!: Pick<IServiceCtx, 'service1' | 'serviceN'>
}

const ctx: IServiceCtx = {
	service1: new Service1(),
	service2: new Service2(),
	// ....
	serviceN: new ServiceN(),
}
Object.values(ctx).forEach(instance => {
	instance.ctx = ctx;
});

Для большей унификации можно воспользоваться следующим сниппетом кода.

export function injector<T>(
	init: { [K in keyof T]: () => T[K]; },
	inject: (instance: T[keyof T], val: T) => void,
): T {
	const result = {} as Record<string, unknown>;
	Object.entries(init).forEach(([key, fn]) => {
		result[key] = (fn as () => unknown)();
	});
	Object.values(result).forEach(ref => {
		inject(ref as T[keyof T], result as T);
	});
	return result as T;
}


const ctx = injector<IServiceCtx>({
	service1: new Service1(),
	service2: new Service2(),
	// ....
	serviceN: new ServiceN(),
}, (instance, ctx) => {
	instance.inject = ctx;
	// здесь можно сделать какие-то дополнительные действия
});