Skip to content

Instantly share code, notes, and snippets.

@waynebloss
Created January 24, 2024 17:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save waynebloss/28fd81a00eec98a67007bec40473a71b to your computer and use it in GitHub Desktop.
Save waynebloss/28fd81a00eec98a67007bec40473a71b to your computer and use it in GitHub Desktop.
Using TSyringe without emitDecoratorMetadata (Preview)

A system for using TSyringe without emitDecoratorMetadata

Some people want to use TSyringe without enabling the tsconfig emitDecoratorMetadata compiler option that allows decorators such as @injectable() to compile properly.

The way to do this is simply to make the same calls to Reflect.decorate() and Reflect.metadata() that would be compiled for @injectable() and so forth.

At the heart of this system is the registerInjectable function which relies on getDecorationParams to do most of the work.

One bonus feature of this system is that we allow for some very nice intellisense autocompletes when registering and resolving dependencies.

import {
DependencyContainer,
InjectionToken,
RegistrationOptions,
baseContainer,
constructor,
delay,
injectable,
registry,
} from "./driver";
import { DependencySpec, getDecorationParams } from "./core";
// #region Dependency Acquisition
/**
* Gets a named dependency from the container. See also
* {@link acquire.all} and {@link acquire.many}
*/
export interface IAcquireFunction<TDeps> {
<K extends DepId<TDeps>>(id: K): TDeps[K];
/** Gets all of the named dependencies from the container. */
all: IAcquireAllFunction<TDeps>;
/**
* Gets the registered implementations of the given dependency id.
*/
many: IAcquireManyFunction<TDeps>;
}
/** Gets all of the named dependencies from the container. */
export interface IAcquireAllFunction<TDeps> {
<K extends DepId<TDeps>>(
ids: Array<K>,
): {
[E in Array<K>[number]]: TDeps[E];
};
}
/**
* Gets the registered implementations of the given dependency id.
*/
export interface IAcquireManyFunction<TDeps> {
<K extends DepId<TDeps>>(id: K): Array<TDeps[K]>;
}
// #endregion
// #region Loaders
/**
* A {@link target} class to load by making it {@link injectable} along with
* it's {@link dependencies}.
*/
export interface PlatformTypeLoad<TDeps, T = any> {
target: constructor<T>;
dependencies?: Dep<TDeps>[];
}
/** Function that produces a {@link PlatformTypeLoad}. */
export type PlatformTypeLoader<TDeps, T = any> = () => PlatformTypeLoad<
TDeps,
T
>;
// #endregion
// #region Utility
// - Types that couldn't be nested inside the generic DIContainer.
//
/** Key of `TDeps` restricted to `string` keys only. */
type DepId<TDeps> = Extract<keyof TDeps, string>;
/** Dependency key or type or a specification for loading one by key or type. */
type Dep<TDeps> = DependencySpec<DepId<TDeps>>;
// #endregion
/**
* A Dependency Injection container with automatic dependency id and TypeScript
* type lookup in registration and resolution methods.
*
* Pass a blank interface type as `TDeps` and then let your services modules
* use TypeScript module augmentation to add things to that interface, e.g.
* `declare module "@/my/container" { interface MyDeps { ... } }`.
* See examples for more details.
*/
export class DIContainer<TDeps> {
private impl: DependencyContainer = baseContainer;
constructor() {
this.acquire.all = this.acquireAll;
this.acquire.many = this.acquireMany;
}
/**
* Makes a {@link DIContainer} a child of this one. The given `container`
* MUST NOT have had any types registered with it before adoption.
*
* This pattern simplifies the work that the TypeScript parser must do to
* track the source and types of the `DIContainer` methods.
*/
adoptChild(container: DIContainer<unknown>) {
const child = this.impl.createChildContainer();
container.impl = child;
}
// #region Dependency Acquisition
/**
* Gets a named dependency from the container. See also
* {@link acquire.all} and {@link acquire.many}
*/
public readonly acquire = ((id) => {
return this.impl.resolve(id);
}) as IAcquireFunction<TDeps>;
public readonly acquireAll: IAcquireAllFunction<TDeps> = (ids) => {
const acquired: ReturnType<IAcquireAllFunction<TDeps>> = {} as any;
for (const id of ids) {
// Guard against acquiring anything twice
acquired[id] = acquired[id] ?? this.impl.resolve(id);
// CONSIDER: Throwing an error for duplicate ids at DEV time.
// NOTE: TypeScript can't EASILY be used to type an array of unique ids.
}
return acquired;
};
public readonly acquireMany: IAcquireManyFunction<TDeps> = (id) => {
return this.impl.resolveAll(id);
};
/**
* Resolves a string, symbol, class or delayed dependency from the container.
*/
public readonly resolve = <T = any>(token: InjectionToken<T>) => {
return this.impl.resolve<T>(token);
};
/**
* Resolves all registered implementations of the given string, symbol, class
* or delayed constructor dependency token.
*/
public readonly resolveMany = <T = any>(token: InjectionToken<T>): T[] => {
return this.impl.resolveAll<T>(token);
};
// #endregion
// #region Register Types
/**
* Registers an anonymous `class` which is **transient** by default, i.e. it
* will be newly created for every dependent. Optionally pass a different
* {@link Lifecycle} in the 3rd argument.
* @param target The class to register with container.
* @param dependencies The container items to be injected this class's
* constructor. See {@link PlatformDependency} for examples.
*/
public readonly registerClass = <T>(
target: constructor<T>,
dependencies: Dep<TDeps>[] = [],
options?: RegistrationOptions,
) => {
this.registerInjectable(target, dependencies);
this.impl.register<T>(
target,
{
useClass: target,
},
options,
);
};
/**
* Registers an injectable class by applying the tsyringe {@link injectable}
* decorator to the given `target` class including the `@inject("dep")`
* decorator for each dependency given.
* See https://github.com/microsoft/tsyringe#injectable
* @param target The class to register with container.
* @param dependencies The container items to be injected this class's
* constructor. See {@link PlatformDependency} for examples.
*/
public readonly registerInjectable = <T = any>(
target: constructor<T>,
dependencies: Dep<TDeps>[] = [],
) => {
const { delayed, params, paramtypes } = getDecorationParams(dependencies);
if (params.length > 0) {
// const x = delayed ? registry(delayed) : undefined;
Reflect.decorate(
[
injectable() as ClassDecorator,
...(delayed ? [registry(delayed)] : []),
...params,
Reflect.metadata("design:paramtypes", paramtypes),
],
target,
);
}
};
/**
* Registers a **singleton** class. It will ONLY be created ONCE and the same
* instance will be given to all dependents.
* @param target The class to register with container.
* @param dependencies The container items to be injected this class's
* constructor. See {@link PlatformDependency} for examples.
*/
public readonly registerSingleton = <T = any>(
// TODO: target should be constructor<any> | InjectionToken<any>...
target: constructor<T>,
dependencies: Dep<TDeps>[] = [],
) => {
this.registerInjectable(target, dependencies);
this.impl.registerSingleton(target);
};
/**
* Register a token which is a redirect or an alias. It simply states that
* given token `x`, resolve using token `y`.
* See https://github.com/microsoft/tsyringe#token-provider
*/
public readonly registerToken = <T = any>(
target: InjectionToken<T>,
token: InjectionToken<T>,
options?: RegistrationOptions,
) => {
this.impl.register<T>(
target,
{
useToken: token,
},
options,
);
};
// #endregion
// #region Register Factories, Loaders
/** Registers a named factory *(of any type)* or a `class` factory. */
public readonly registerFactory = <T = any>(
idOrClass: DepId<TDeps> | constructor<T>,
factory: (dependencyContainer: DependencyContainer) => T,
) => {
this.impl.register<T>(idOrClass, {
useFactory: factory,
});
};
/**
* Creates a named, *delayed loader* for a *dynamic* `class`.
* @param id Id of the loaded type as it will be used by dependents.
* @param loader Function to return target class and dependencies.
* @example
* registerLoader(
* "thing",
* () => {
* console.log("Now setting up type DynamicProvider...");
* class DynamicProvider {
* constructor(private logger: ILogger) {}
* getConfig(name: string) {
* this.logger.info(`Getting "${name}" config...`);
* return `${name}.config`;
* }
* }
* return {
* target: import.meta.env.DEV ? DynamicProvider : StandardProvider,
* dependencies: ["logger"],
* };
* },
* { lifecycle: Lifecycle.Singleton },
* );
*/
public readonly registerLoader = <T = any>(
id: DepId<TDeps>,
loader: PlatformTypeLoader<TDeps, T>,
options?: RegistrationOptions,
) => {
this.impl.register<T>(
id,
{
useToken: delay(() => {
const { target, dependencies } = loader();
this.registerInjectable<T>(target, dependencies);
return target;
}),
},
options,
);
};
// #endregion
// #region Register Providers
/**
* Registers a named provider class which is **transient** by default, i.e.
* it will be newly created for every dependent. Optionally pass a different
* {@link Lifecycle} in the 4th argument.
* @param id Id of the provider as it will be used by dependents.
* @param target The class to register with container.
* @param dependencies The container items to be injected this class's
* constructor. See {@link PlatformDependency} for examples.
*/
public readonly registerProvider = <T>(
id: DepId<TDeps>,
target: constructor<T>,
dependencies: Dep<TDeps>[] = [],
options?: RegistrationOptions,
) => {
this.registerInjectable(target, dependencies);
this.impl.register<T>(
id,
{
useClass: target,
},
options,
);
};
// #endregion
// #region Register Services
/**
* Registers a **singleton** service class. The {@link target} `class` will
* ONLY be created ONCE.
* @param id Id of the service as it will be used by dependents.
* @param target The class that will be instantiated by the platform container.
* @param dependencies The container items to be injected this class's
* constructor. See {@link PlatformDependency} for examples.
*/
public readonly registerService = <T = any>(
id: DepId<TDeps>,
target: constructor<T>,
dependencies: Dep<TDeps>[] = [],
) => {
this.registerInjectable(target, dependencies);
this.impl.registerSingleton(id, target);
};
// #endregion
// #region Register Values
/** Registers named `value` *(of any type)* or a `class` instance. */
public readonly registerValue = <K extends DepId<TDeps>, T = any>(
id: K | constructor<T>,
value: TDeps[K] | T,
) => {
this.impl.register(id, {
useValue: value,
});
};
// #endregion
}
import {
DelayedConstructor,
InjectionToken,
Transform,
inject,
injectAll,
injectAllWithTransform,
injectWithTransform,
constructor,
delay,
registry,
} from "./driver";
/** `InjectionToken<T>` excluding `string` and `symbol`. */
export type ConstructorToken<T = any> = constructor<T> | DelayedConstructor<T>;
/**
* A key or specification of dependency to import and how to import it.
* @example
* class MyClass {
* constructor(
* fld1: IDep1,
* fld2: IDep2[],
* yesno: boolean,
* ids: string[],
* yadaProvider: YadaProvider,
* things: ThingClass[],
* ) {}
* }
* registerClass(MyClass, [
* "dep1", // Inject dep1 by id.
* ["dep2"], // ...array of dep2 by id.
* ["dep3", getYesNo, ...yesNoArgs], // ...dep3 transformed to bool.
* [Dep4, [getId, ...getIdArgs]], // ...dep4 array transformed to string.
* YadaProvider, // ...exact type.
* ]);
*/
export type DependencySpec<T extends string> =
// A registered platform [Factory|Provider|Service|Value] id.
| T
// An exact class type.
| ConstructorToken
// An array of registered [Factory|Provider|Service|Value] id.
| [T]
// For `injectWithTransformer`, e.g. `[Dep|"depName", transformer, ...args]`
// or `delayed(()=>DelayedType)`, e.g. `[Dep|"dep", () => MyTypeName]`
| [
T | ConstructorToken,
InjectionToken<Transform<any, any>> | TypeLoader,
...transformerArgs: any[],
]
// For `injectAllWithTransformer`, e.g. `[Dep|"dep", [transformer, ...args]]`
| [T, [InjectionToken<Transform<any, any>>, ...transformerArgs: any[]]];
/** Generic of `Platform[Factory|Provider|Service|Value]Id` `| MyClass` */
type DependencyToken = string | ConstructorToken<any>;
/** Function that returns a class. */
export type TypeLoader<T = any> = Parameters<typeof delay<T>>[0];
/** Decorates a constructor param. */
export function cparam(paramIndex: number, decorator: any) {
return function (target: any, key: string | symbol | undefined) {
decorator(target, key, paramIndex);
};
}
/**
* Returns values to use with `Reflect.decorate()` on an injectable class with
* optional dependencies to be injected into it's constructor. See example.
* @example
* // How TypeScript compiles `@decorator()` metadata:
* Reflect.decorate(
* [
* injectable(),
* cparam(0, inject("IFoo")),
* cparam(1, inject("Bar")),
* cparam(2, injectAll("Things")),
* Reflect.metadata("design:paramtypes", [Object, Bar, Array]),
* ]
* );
* // So in our register[Provider|Service]() functions, we do the following.
* // See those functions for documentation on how to call them with a tsyringe
* // `Transformer`, for when you want to inject but transform some dependency.
* const { params, paramtypes } = getDecorationParams(dependencies);
* if (params.length > 0) {
* Reflect.decorate(
* [
* injectable() as ClassDecorator,
* ...params,
* Reflect.metadata("design:paramtypes", paramtypes),
* ],
* target,
* );
* }
*
* @param dependencies Dependencies of the registration target.
* @returns Values ready to use with the (`n>0`)th `Reflect.decorate()` args.
*/
export function getDecorationParams<
// Generic of `Platform[Factory|Provider|Service|Value]Id`
K extends string = string,
// Generic of `PlatformDependency`
P extends DependencySpec<K> = DependencySpec<K>,
>(
dependencies: P[],
): {
delayed?: Parameters<typeof registry>[0];
params: ReturnType<typeof cparam>[];
paramtypes: any[];
} {
/** Constructor params */
const cparams: ReturnType<typeof cparam>[] = [];
const metadataValue: any[] = [];
/** Delayed params */
let dparams: Parameters<typeof registry>[0] | undefined = undefined;
/** Current dependency index */
let depIdx = 0;
for (const dep of dependencies) {
const depIsArray = Array.isArray(dep);
let target: DependencyToken | undefined = undefined;
/**
* A constructor for `inject` which defaults to `Object`, the type used
* internally as a placeholder for registered TS interfaces...
*/
let value: any = undefined;
let isArray = false;
let loader: TypeLoader | undefined = undefined;
let transformer: InjectionToken<Transform<[any], any>> | undefined =
undefined;
let transformerArgs: any[] = [];
if (!depIsArray) {
// This dep is a single dependency id/target, e.g. `Dep|"dep"`.
target = dep;
} else {
// This dep is an array which represents a specification.
const { length } = dep;
// Decide if dep is describing:
// - injectAll? e.g. `["dep"]`
// - injectAllWithTransform e.g. `["dep", [transform, ...args]]`
// - injectWithTransform? e.g. `["dep", transformer, ...args]`
switch (length) {
case 0:
throw new Error(`Empty spec, dependency ${depIdx}`);
case 1:
isArray = true;
target = dep[0];
break;
default:
target = dep[0];
if (Array.isArray(dep[1])) {
isArray = true;
transformer = dep[1][0];
transformerArgs = dep[1].slice(1);
} else if (isTransformer(dep[1])) {
transformer = dep[1];
transformerArgs = dep.slice(1);
} else if (typeof dep[1] === "function") {
// Load delayed interface...
// See https://github.com/microsoft/tsyringe?tab=readme-ov-file#interfaces-and-circular-dependencies
loader = dep[1];
dparams = dparams ?? [];
dparams.push({
token: target!,
useToken: delay(loader!),
});
} else {
throw new Error(`Expected transformer spec, dependency ${depIdx}`);
}
}
if (isArray) {
// Set the type we will now give to "design:paramtypes".
value = Array;
}
}
if (!target) {
throw new Error(`Missing dependency ${depIdx}`);
}
if (!depIsArray || !value) {
if (transformer) {
cparams.push(
// Inject a constructor dep with transformer.
cparam(
depIdx,
injectWithTransform(target, transformer, ...transformerArgs),
),
);
} else {
cparams.push(
// Inject a constructor dep.
cparam(depIdx, inject(target)),
);
}
} else if (isArray) {
if (transformer) {
cparams.push(
// Inject a constructor array dep with transformer.
cparam(
depIdx,
injectAllWithTransform(target, transformer, ...transformerArgs),
),
);
} else {
cparams.push(
// Inject a constructor array dep.
cparam(depIdx, injectAll(target)),
);
}
}
// Set the "design:paramtypes" metadata value.
// `?? Object` sets the default placeholder type for registered interfaces.
metadataValue.push(value ?? Object);
depIdx += 1;
}
return {
delayed: dparams,
params: cparams,
paramtypes: metadataValue,
};
// The returned values should be ready to use with `Reflect.decorate()`
// e.g.
// Reflect.decorate(
// [
// cparam(0, inject("IFoo")),
// cparam(1, inject("Bar")),
// cparam(2, injectAll("Things")),
// Reflect.metadata("design:paramtypes", [Object, Bar, Array]),
// ]
// );
}
/** `true` if `It` is a `Transform` token. */
export function isTransformer(
It: any,
): It is InjectionToken<Transform<[any], any>> {
return typeof It?.prototype?.transform === "function";
}
/** @file SAMPLE code with unchanged import paths, from a VITE/REACT app. */
import { Disposable } from "@/base/types";
import { Lifecycle, delay } from "@/base/di";
import { Registry } from "@/platform/registry";
import { ILogger } from "@/platform/logger/LoggerService";
const {
acquire,
registerClass,
registerInjectable,
registerLoader,
registerProvider,
registerService,
registerToken,
} = Registry.of("platform.container");
declare module "@/platform/container" {
interface IDependencies {
thing: ConfigThing;
dbManager: IDatabaseManager;
}
}
let configs = 0;
export class ConfigThing {
constructor(private logger: ILogger) {
configs += 1;
}
getConfig(name: string) {
this.logger.info(`Getting "${name}" config...`);
return `${name}.config${configs}`;
}
}
// registerInjectable(ConfigThing, ["logger"]);
// registerClass(ConfigThing, ["logger"], {
// lifecycle: Lifecycle.Singleton,
// });
// registerProvider("thing", ConfigThing, ["logger"]);
// registerToken(
// "thing",
// delay(() => {
// class DynamicProvider {
// constructor(private logger: ILogger) {}
// getConfig(name: string) {
// this.logger.info(`Getting "${name}" config...`);
// return `${name}.config${configs}`;
// }
// }
// registerInjectable(DynamicProvider, ["logger"]);
// return DynamicProvider;
// }),
// { lifecycle: Lifecycle.Singleton },
// );
registerLoader(
"thing",
() => {
console.log("Now setting up type ConfigThing...");
class DynamicProvider {
constructor(private logger: ILogger) {}
getConfig(name: string) {
this.logger.info(`Getting "${name}" config...`);
return `${name}.config${configs}`;
}
}
return {
target: DynamicProvider,
dependencies: ["logger"],
};
},
{ lifecycle: Lifecycle.Singleton },
);
export interface IDatabaseManager extends Disposable {
dots?: string;
connect(): Promise<boolean>;
}
export class DatabaseManager implements IDatabaseManager {
public dots?: string;
constructor(
private logger: ILogger,
private config: ConfigThing,
) {}
async connect() {
const config = this.config.getConfig("connection");
this.logger.info(`Connecting${this.dots ?? "..."}`, config);
return Promise.resolve(true);
}
dispose() {
this.dots = ">>>";
}
}
registerProvider("dbManager", DatabaseManager, [
"logger",
"thing",
// ConfigThing,
// delay(() => ConfigThing),
// ["thing", () => ConfigThing],
]);
setTimeout(async () => {
const { dbManager: dbm, logger } = acquire.all(["dbManager", "logger"]);
const connected = await dbm.connect();
logger.info(connected ? "OK" : "Failed");
// let xyz: any = dbm;
// if (isDisposable(xyz)) {
// xyz.dispose();
// }
dbm.dots = "+++";
}, 3000);
setTimeout(async () => {
const { dbManager: dbm, logger } = acquire.all(["dbManager", "logger"]);
const connected = await dbm.connect();
logger.info(connected ? "OK" : "Failed");
}, 6000);
// Objects to alias...
import {
//
container as baseContainer,
} from "tsyringe";
// Aliased objects...
export {
//
baseContainer,
};
// Unaliased objects...
export {
//
Lifecycle,
autoInjectable,
delay,
inject,
injectable,
injectAll,
injectAllWithTransform,
injectWithTransform,
instanceCachingFactory,
instancePerContainerCachingFactory,
isClassProvider,
isFactoryProvider,
isNormalToken,
isTokenProvider,
isValueProvider,
predicateAwareClassFactory,
registry,
scoped,
singleton,
} from "tsyringe";
// Main types...
export type {
//
ClassProvider,
DependencyContainer,
//
// The Disposable interface is exported from src/base/types instead, because
// it's a very basic lifecycle interface which may be useful there.
// (This is also similar to how the VS Code src is organized.)
//
// Disposable, // Do not export. Already exported from src/base/types/...
//
FactoryFunction,
FactoryProvider,
Frequency,
InjectionToken,
Provider,
RegistrationOptions,
TokenProvider,
ValueProvider,
} from "tsyringe";
// Non-default types...
export type { constructor, Transform } from "tsyringe/dist/typings/types";
export type { DelayedConstructor } from "tsyringe/dist/typings/lazy-helpers";
/** @file SAMPLE code with unchanged import paths, from a VITE/REACT app. */
import { Registry } from "@/platform/registry";
const { registerService } = Registry.of("platform.container");
declare module "@/platform/container" {
interface IDependencies {
logger: ILogger;
}
}
export interface ILogger {
trace(message: string, ...args: any[]): void;
debug(message: string, ...args: any[]): void;
info(message: string, ...args: any[]): void;
warn(message: string, ...args: any[]): void;
error(message: string | Error, ...args: any[]): void;
}
export class LoggerService implements ILogger {
trace(message: string, ...args: any[]) {
console.trace(message, ...args);
}
debug(message: string, ...args: any[]) {
console.debug(message, ...args);
}
info(message: string, ...args: any[]) {
console.info(message, ...args);
}
warn(message: string, ...args: any[]) {
console.warn(message, ...args);
}
error(message: string | Error, ...args: any[]) {
console.error("" + message, ...args);
}
}
registerService("logger", LoggerService);
/** @file SAMPLE code with unchanged import paths, from a VITE/REACT app. */
// Polyfill for the Metadata Reflection API needed by @/platform/container.
import "@abraham/reflection";
// Local
// Load our core services...
import "@/platform/container";
import "@/platform/logger/LoggerService";
import "@/features/db/DatabaseManager";
/**
* The application chunks to load in parallel.
*
* `render.tsx` contains the main dependencies and providers of the base app.
* - React, ReactDom, ThemeProvider, StyledEngineProvider, MUI-core, etc.
* `App.tsx` contains the main structure of the base app.
*
* These are the two main chunks that are used to render the core structure of
* the app. Importing them with `Promise.all` (by using HTTP/2 multiplexing) we
* can load them in parallel and achieve the best possible performance.
*/
const chunks = [
// Main dependencies, providers, etc...
import("@/app/render"),
// App specific structure...
import("@/workbench"),
];
Promise.all(chunks).then(([{ default: render }, { default: Workbench }]) => {
render(Workbench);
});
// ts(1208) - Export or import something to make this a module.
// export {};
/**
* @file Modified from https://github.com/microsoft/vscode/blob/0c109dbcdec16d415bebe5ef2ff7b1e8c9cf9d14/src/vs/platform/registry/common/platform.ts
* - Renamed IRegistry.as to IRegistry.of
* - Made IRegistry generic and added IRegistries module augmentation interface.
*/
// TODO: Get the following code from
// https://github.com/microsoft/vscode/blob/0c109dbcdec16d415bebe5ef2ff7b1e8c9cf9d14/src/vs/base/...
import * as Assert from "@/base/assert";
import * as Validate from "@/base/types/validate";
/**
* Interface to declare types of registries onto with module augmentation.
* @example
* declare module "@/provider/registry" {
* interface IRegistries {
* "my.registry": IAnyType;
* }
* }
*/
export interface IRegistries {
// e.g. "platform.container": DIContainer<IDependencies>;
}
/** Manages a map of registrations. */
export interface IRegistry<R> {
/**
* Adds a new type of registry with a unique `id`.
* @param id a unique identifier
* @param data a contribution
*/
add<K extends keyof R>(id: K, data: R[K]): void;
/**
* Returns true if there is a registry with the provided id.
* @param id an registry identifier
*/
knows<K extends keyof R>(id: K): boolean;
/**
* Returns the registry defined by the specified id, or null.
* @param id an registry identifier
*/
of<K extends keyof R>(id: K): R[K];
}
class RegistryImpl implements IRegistry<IRegistries> {
private readonly data = new Map<string, any>();
public add(id: string, data: any): void {
Assert.ok(Validate.isString(id));
Assert.ok(Validate.isObject(data));
Assert.ok(!this.data.has(id), "There is already a registry with this id");
this.data.set(id, data);
}
public knows(id: string): boolean {
return this.data.has(id);
}
public of<K extends keyof IRegistries>(id: K): IRegistries[K] {
return this.data.get(id) || null;
}
}
export const Registry: IRegistry<IRegistries> = new RegistryImpl();
import React from "react";
// Local
// import { Registry } from "./registry";
import { IDependencies, DependencyId, myContainer } from "./test";
const { acquireAll } = myContainer; // Registry.of("my.container");
/**
* Hook to allow React components to use the factories, loaders, providers and
* services which have been registered within the platform. **NOTE: This
* hook returns stable values** and should be added to the list of stable hooks
* in your eslint rules `react-hooks/exhaustive-deps`. See example below.
* @example
* "react-hooks/exhaustive-deps": [
* "warn",
* {
* stableHooksPattern: "useMy|useAbc|useXyz",
* // ignoredDepsPattern: '^navigation|navigate|popToTop|pop$',
* },
* ],
*/
export function useMy<K extends DependencyId, Ids extends Array<K>>(
...ids: Ids
): {
[E in Ids[number]]: IDependencies[E];
} {
return React.useMemo(
() => {
return acquireAll(ids);
},
// eslint-disable-next-line
[],
);
}
import { DIContainer } from "./container";
// import { Registry } from "./registry";
/**
* Interface to declare dependency types onto.
* @example
* declare module "@/my/container" {
* interface IDependencies {
* myValue: string;
* myProvider: IMyProvider;
* myService: IMyService;
* }
* }
*/
export interface IDependencies {
/** Test `boolean` value. */
bval: boolean;
/** Test `number` value. */
nval: number;
/** Test `string` value. */
sval: string;
}
/** A key of {@link IDependencies} */
export type DependencyId = keyof IDependencies;
/**
* **Direct access** *to a container that SHOULD normally be accessed indirectly*
* via something like `Registry.of("my.container")`
*/
export const myContainer = new DIContainer<IDependencies>();
// TODO: Expose the container via Registry so our contained instances are testable,
// since that code won't be importing myContainer directly...
// declare module "@/my/registry" {
// interface IRegistries {
// "my.container": DIContainer<IDependencies>;
// }
// }
// Registry.add("my.container", myContainer);
// See registry.ts below...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment