@stone-js/service-container
Version:
Javascript/Typescript IoC Service Container with proxy resolver and destructuring injection
529 lines (519 loc) • 19.9 kB
TypeScript
/**
* Class representing a Proxiable.
*
* This class allows instances to be wrapped in a Proxy, enabling custom behaviors for property access, assignment, etc.
*
* @author Mr. Stone <evensstone@gmail.com>
*/
declare abstract class Proxiable {
/**
* Creates a Proxiable instance wrapped in a Proxy.
*
* @param handler - A trap object for the proxy, which defines custom behavior for fundamental operations (e.g., property lookup, assignment, etc.).
* @returns A new proxy object for this instance.
*/
constructor(handler: ProxyHandler<Proxiable>);
}
/**
* A resolver function that takes a container and returns a value of type V.
*
* @template V - The type of value that the resolver returns.
* @param container - The container used to resolve dependencies.
* @returns The resolved value of type V.
*
* @example
* ```typescript
* const myResolver: Resolver<number> = (container: IContainer) => {
* // Use the container to resolve dependencies and return a number.
* return 42;
* };
* ```
*/
type Resolver<V> = (container: IContainer) => V;
/**
* A union type representing the possible keys that can be used to bind values in the container.
*
* Binding keys can be of various types, such as numbers, booleans, strings, functions, objects, or symbols.
* These types are used because they provide a broad range of ways to uniquely identify a binding.
*
* - `number`, `boolean`, `string`: These are basic types that are easy to use and uniquely identify a binding.
* - `Function`: Useful for identifying bindings by constructor or other functions.
* - `object`: Allows more complex key types, like instances of classes.
* - `symbol`: Guarantees a unique identifier, which can prevent conflicts.
*
* @example
* ```typescript
* const key1: BindingKey = 42; // Using a number as a key
* const key2: BindingKey = 'serviceName'; // Using a string as a key
* const key3: BindingKey = Symbol('uniqueKey'); // Using a symbol for uniqueness
* const key4: BindingKey = MyServiceClass; // Using a function (constructor) as a key
* const key5: BindingKey = { custom: 'objectKey' }; // Using an object as a key
* ```
*/
type BindingKey = number | boolean | string | Function | object | symbol;
/**
* A union type representing the possible values that can be bound in the container.
*
* Binding values can be of various types, including numbers, booleans, strings, functions, objects, or symbols.
* Unlike `BindingKey`, `BindingValue` represents the actual data or instance being bound, while `BindingKey` represents the identifier used to access that data.
*/
type BindingValue = number | boolean | string | Function | object | symbol;
/**
* Interface representing a Binding.
*
* This interface defines the contract for all types of bindings in the service container.
* Bindings are used to manage dependencies and control how objects are instantiated within the container.
*
* @template V - The type of value that this binding holds.
* @author Mr. Stone <evensstone@gmail.com>
*/
interface IBinding<V extends BindingValue> {
/**
* Resolve and return the value of the binding.
*
* @param container - The container to resolve dependencies from.
* @returns The resolved value of the binding.
*/
resolve: (container: IContainer) => V | undefined;
}
/**
* Interface representing a Container.
*
* This interface defines the public contract for dependency injection containers,
* allowing for better testability and preventing circular dependencies.
*
* @author Mr. Stone <evensstone@gmail.com>
*/
interface IContainer {
/**
* Retrieve the value of the bindings property.
*/
getBindings: () => Map<BindingKey, IBinding<BindingValue>>;
/**
* Retrieve the value of the aliases property.
*/
getAliases: () => Map<string, BindingKey>;
/**
* Set a binding as alias.
*/
alias: (key: BindingKey, aliases: string | string[]) => this;
/**
* Check if an alias exists in the container.
*/
isAlias: (alias: BindingKey) => boolean;
/**
* Get a binding key by its alias.
*/
getAliasKey: (alias: BindingKey) => BindingKey | undefined;
/**
* Bind a single instance or value into the container under the provided key.
*/
instance: (key: BindingKey, value: BindingValue) => this;
/**
* Bind a single instance or value into the container under the provided key if not already bound.
*/
instanceIf: (key: BindingKey, value: BindingValue) => this;
/**
* Bind a resolver function into the container under the provided key as a singleton.
*/
singleton: <V extends BindingValue>(key: BindingKey, resolver: Resolver<V>) => this;
/**
* Bind a resolver function into the container under the provided key as a singleton if not already bound.
*/
singletonIf: <V extends BindingValue>(key: BindingKey, resolver: Resolver<V>) => this;
/**
* Bind a resolver function into the container under the provided key, returning a new instance each time.
*/
binding: <V extends BindingValue>(key: BindingKey, resolver: Resolver<V>) => this;
/**
* Bind a resolver function into the container under the provided key, returning a new instance each time if not already bound.
*/
bindingIf: <V extends BindingValue>(key: BindingKey, resolver: Resolver<V>) => this;
/**
* Resolve a registered value from the container by its key.
*/
make: <V extends BindingValue>(key: BindingKey) => V;
/**
* Resolve a value from the container by its key, binding it if necessary.
*/
resolve: <V extends BindingValue>(key: BindingKey, singleton?: boolean) => V;
/**
* Resolve a value from the container by its key and return it in a factory function.
*/
factory: <V extends BindingValue>(key: BindingKey) => () => V;
/**
* Check if a value is already bound in the container by its key.
*/
bound: (key: BindingKey) => boolean;
/**
* Check if a value is already bound in the container by its key.
*/
has: (key: BindingKey) => boolean;
/**
* Reset the container so that all bindings are removed.
*/
clear: () => this;
/**
* AutoBind value to the service container.
*/
autoBinding: <V extends BindingValue>(name: BindingKey, item?: V, singleton?: boolean, alias?: string | string[]) => this;
}
/**
* Abstract class representing a Binding.
*
* This abstract class serves as the base class for all types of bindings in the service container. It holds a value and provides an abstract method
* to resolve and return that value, allowing different subclasses to implement their own resolution logic. Bindings are used to manage dependencies
* and control how objects are instantiated within the container.
*
* @template V - The type of value that this binding holds.
* @author Mr. Stone <evensstone@gmail.com>
*/
declare abstract class Binding<V extends BindingValue> implements IBinding<V> {
/**
* The value held by the binding.
*
* This value is resolved at runtime, either directly or through a resolver function.
*/
protected value?: V;
/**
* Create a new instance of Binding.
*
* @param value - The value to be held by the binding.
*/
constructor(value?: V);
/**
* Check if the value has been resolved.
*
* @returns A boolean indicating whether the value has been resolved.
*/
protected isResolved(): boolean;
/**
* Resolve and return the value of the binding.
*
* This abstract method must be implemented by subclasses to provide specific resolution logic.
*
* @param container - The container to resolve dependencies from.
* @returns The resolved value of the binding.
*/
abstract resolve(container: IContainer): V | undefined;
}
/**
* Class representing a Container.
*
* The Container class acts as a dependency injection container, managing bindings and resolving instances.
* It supports different types of bindings, such as singletons, factories, and instances, and allows the use of aliases for bindings.
* This makes it easier to manage and resolve complex dependency trees in an application.
*
* @author Mr. Stone <evensstone@gmail.com>
*/
declare class Container extends Proxiable implements IContainer {
private readonly aliases;
private readonly resolvingKeys;
private readonly bindings;
/**
* Create a Container.
*
* @returns A new Container instance.
*/
static create(): Container;
/**
* Create a ProxyHandler for the container.
*
* @returns A new ProxyHandler instance.
*/
private static Proxyhandler;
/**
* Create a container.
*
* Initializes the container with empty alias and binding maps.
*/
protected constructor();
/**
* Retrieve the value of the bindings property.
*
* @returns A map of all bindings registered in the container.
*/
getBindings(): Map<BindingKey, Binding<BindingValue>>;
/**
* Retrieve the value of the aliases property.
*
* @returns A map of all aliases registered in the container.
*/
getAliases(): Map<string, BindingKey>;
/**
* Set a binding as alias.
*
* Adds one or more aliases for a given binding key.
*
* @param key - The binding value.
* @param aliases - One or more strings representing the aliases.
* @returns The container instance.
*/
alias(key: BindingKey, aliases: string | string[]): this;
/**
* Check if an alias exists in the container.
*
* @param alias - The alias to check.
* @returns True if the alias exists, false otherwise.
*/
isAlias(alias: BindingKey): boolean;
/**
* Get a binding key by its alias.
*
* @param alias - The alias name.
* @returns The binding key associated with the alias, or undefined if not found.
*/
getAliasKey(alias: BindingKey): BindingKey | undefined;
/**
* Bind a single instance or value into the container under the provided key.
*
* @param key - The key to associate with the value.
* @param value - The value to be bound.
* @returns The container instance.
*/
instance(key: BindingKey, value: BindingValue): this;
/**
* Bind a single instance or value into the container under the provided key if not already bound.
*
* @param key - The key to associate with the value.
* @param value - The value to be bound.
* @returns The container instance.
*/
instanceIf(key: BindingKey, value: BindingValue): this;
/**
* Bind a resolver function into the container under the provided key as a singleton.
*
* The resolver function will be called once, and the resulting value will be cached for future use.
*
* @param key - The key to associate with the singleton value.
* @param resolver - The resolver function to provide the value.
* @returns The container instance.
*/
singleton<V extends BindingValue>(key: BindingKey, resolver: Resolver<V>): this;
/**
* Bind a resolver function into the container under the provided key as a singleton if not already bound.
*
* @param key - The key to associate with the singleton value.
* @param resolver - The resolver function to provide the value.
* @returns The container instance.
*/
singletonIf<V extends BindingValue>(key: BindingKey, resolver: Resolver<V>): this;
/**
* Bind a resolver function into the container under the provided key, returning a new instance each time.
*
* @param key - The key to associate with the value.
* @param resolver - The resolver function to provide the value.
* @returns The container instance.
*/
binding<V extends BindingValue>(key: BindingKey, resolver: Resolver<V>): this;
/**
* Bind a resolver function into the container under the provided key, returning a new instance each time if not already bound.
*
* @param key - The key to associate with the value.
* @param resolver - The resolver function to provide the value.
* @returns The container instance.
*/
bindingIf<V extends BindingValue>(key: BindingKey, resolver: Resolver<V>): this;
/**
* Resolve a registered value from the container by its key.
*
* @param key - The key to resolve.
* @returns The resolved value.
* @throws ContainerError if the key cannot be resolved.
*/
make<V extends BindingValue>(key: BindingKey): V;
/**
* Resolve a value from the container by its key, binding it if necessary.
*
* @param key - The key to resolve.
* @param singleton - Whether to bind as a singleton if not already bound.
* @returns The resolved value.
*/
resolve<V extends BindingValue>(key: BindingKey, singleton?: boolean): V;
/**
* Resolve a value from the container by its key and return it in a factory function.
*
* @param key - The key to resolve.
* @returns A factory function that returns the resolved value.
*/
factory<V extends BindingValue>(key: BindingKey): () => V;
/**
* Check if a value is already bound in the container by its key.
*
* @param key - The key to check.
* @returns True if the key is bound, false otherwise.
*/
bound(key: BindingKey): boolean;
/**
* Check if a value is already bound in the container by its key.
*
* @param key - The key to check.
* @returns True if the key is bound, false otherwise.
*/
has(key: BindingKey): boolean;
/**
* Reset the container so that all bindings are removed.
*
* @returns The container instance.
*/
clear(): this;
/**
* AutoBind value to the service container.
*
* @param name - A key to make the binding. Can be anything.
* @param item - The item to bind.
* @param singleton - Bind as singleton when true.
* @param alias - Key binding aliases.
* @returns The container instance.
*/
autoBinding<V extends BindingValue>(name: BindingKey, item?: V, singleton?: boolean, alias?: string | string[]): this;
}
/**
* Class representing a ContainerError.
*
* @author Mr. Stone <evensstone@gmail.com>
*/
declare class ContainerError extends Error {
/**
* Error type indicating an alias conflict.
*/
static readonly ALIAS_TYPE = "alias";
/**
* Error type indicating that the resolver is not a function.
*/
static readonly RESOLVER_TYPE = "resolver";
/**
* Error type indicating a resolution failure.
*/
static readonly RESOLUTION_TYPE = "resolution";
/**
* Error type indicating an attempt to alias an unbound value.
*/
static readonly ALIAS_UNBOUND_TYPE = "alias_unbound";
/**
* Error type indicating that a value is not a service.
*/
static readonly NOT_A_SERVICE_TYPE = "not_a_service";
/**
* Error type indicating an error thrown by the resolver function.
*/
static readonly CANNOT_RESOLVE_TYPE = "cannot_resolve";
/**
* Error type indicating a circular dependency.
*/
static readonly CIRCULAR_DEPENDENCY_TYPE = "circular_dependency";
/**
* The type of the error.
*/
private readonly type;
/**
* Create a ContainerError.
*
* @param type - The type of the error.
* @param message - The error message or key related to the error.
*/
constructor(type: string, message: BindingKey);
/**
* Retrieve the error message based on the type and provided message.
*
* @param type - The type of the error.
* @param message - The error message or key related to the error.
* @returns The formatted error message.
*/
private getMessage;
/**
* Retrieve the resolution message based on the key.
*
* @param key - The key for which the resolution failed.
* @returns The formatted resolution error message.
*/
private getResolutionMessage;
}
/**
* Class representing a ResolverBinding.
*
* This class extends the Binding class, using a resolver function to lazily resolve the value when needed.
*
* @template V - The type of value that this binding holds.
* @author Mr. Stone <evensstone@gmail.com>
*/
declare abstract class ResolverBinding<V extends BindingValue> extends Binding<V> {
/**
* The resolver function used to provide the binding value.
*
* This function will be called when the value is needed, allowing for lazy instantiation
* and dependency resolution. It should return an instance of type `V`.
*/
protected readonly resolver: Resolver<V>;
/**
* Create a new instance of ResolverBinding.
*
* @param resolver - The resolver function to provide the binding value.
* @throws ContainerError if the resolver is not a function.
*/
constructor(resolver: Resolver<V>);
}
/**
* Class representing a Factory.
*
* The Factory class extends the ResolverBinding class, providing a mechanism to resolve a new instance each time the binding is resolved.
* This ensures that a fresh instance is created with each call to the `resolve` method.
*
* @template V - The type of value that this binding holds.
* @author Mr. Stone <evensstone@gmail.com>
*/
declare class Factory<V extends BindingValue> extends ResolverBinding<V> {
/**
* Resolve and return the value of the binding.
*
* Each time this method is called, a new value is resolved using the resolver function.
* This is intended for cases where a fresh instance is required for each resolution, such as factories or transient dependencies.
*
* @param container - The container to resolve dependencies from.
* @returns The resolved value of the binding.
* @throws ContainerError if the value cannot be resolved.
*/
resolve(container: IContainer): V;
}
/**
* Class representing an Instance.
*
* This class extends the Binding class and directly holds an instance value.
* It provides a straightforward resolution mechanism that simply returns the stored value.
*
* @template V - The type of value that this binding holds.
* @author Mr. Stone <evensstone@gmail.com>
*/
declare class Instance<V extends BindingValue> extends Binding<V> {
/**
* Resolve and return the value of the binding.
*
* @param _container - Container to resolve dependencies (not used in this implementation).
* @returns The resolved value of the binding.
*/
resolve(_container: IContainer): V | undefined;
}
/**
* Class representing a Singleton.
*
* The Singleton class extends the ResolverBinding class, ensuring that the value is only resolved once.
* Subsequent calls to the `resolve` method will return the previously resolved value, making it behave as a singleton.
*
* @template V - The type of value that this binding holds.
* @author Mr. Stone <evensstone@gmail.com>
*/
declare class Singleton<V extends BindingValue> extends ResolverBinding<V> {
/**
* Resolve and return the value of the binding.
*
* If the value has already been resolved, return the cached value. Otherwise, use the resolver function
* to resolve the value, store it, and return it.
*
* @param container - The container to resolve dependencies from.
* @returns The resolved value of the binding.
* @throws ContainerError if the value cannot be resolved.
*/
resolve(container: IContainer): V | undefined;
}
export { Binding, Container, ContainerError, Factory, Instance, Proxiable, ResolverBinding, Singleton };
export type { BindingKey, BindingValue, IBinding, IContainer, Resolver };