From 255acc1e62ad202aa8761b752399dac5defbe2ad Mon Sep 17 00:00:00 2001 From: Alec Gibson <12036746+alecgibson@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:52:42 +0100 Subject: [PATCH] feat: strongly-typed `Container` This is a non-breaking change, affecting only TypeScript types, and doesn't change the implementation in any way. Motivation ========== `inversify` already has some basic support for types when binding, and retrieving bindings. However, the type support requires manual intervention from developers, and can be error-prone. For example, the following code will happily compile, even though the types here are inconsistent: ```ts container.bind('bar').to(Bar); const foo = container.get('bar') ``` Furthermore, this paves the way for [type-safe injection][1], which will be added once this change is in. Improved type safety ==================== This change adds an optional type parameter to the `Container`, which takes an identifier map as an argument. For example: ```ts type IdentifierMap = { foo: Foo; bar: Bar; }; const container = new Container; ``` If a `Container` is typed like this, we now get strong typing both when binding, and getting bindings: ```ts const container = new Container; container.bind('foo').to(Foo); // ok container.bind('foo').to(Bar); // error const foo: Foo = container.get('foo') // ok const bar: Bar = container.get('foo') // error ``` This also has the added benefit of no longer needing to pass around service identifier constants: the strings (or symbols) are all strongly typed, and will fail compilation if an incorrect one is used. Non-breaking ============ This change aims to make no breaks to the existing types, so any `Container` without an argument should continue to work as it did before. [1]: https://github.com/inversify/InversifyJS/issues/788#issuecomment-2209341498 --- CHANGELOG.md | 3 +- src/container/container.ts | 299 +++++++++++++++++------------ src/interfaces/interfaces.ts | 101 ++++++---- src/planning/planner.ts | 8 +- src/syntax/binding_to_syntax.ts | 2 +- test/interfaces/interfaces.test.ts | 108 +++++++++++ wiki/container_typing.md | 21 ++ wiki/readme.md | 1 + 8 files changed, 384 insertions(+), 159 deletions(-) create mode 100644 test/interfaces/interfaces.test.ts create mode 100644 wiki/container_typing.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a12d59bf..65f9f4491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Optional strong typing for `Container` ### Changed ### Fixed -property injection tagged as @optional no longer overrides default values with `undefined`. +property injection tagged as @optional no longer overrides default values with `undefined`. ## [6.0.2] diff --git a/src/container/container.ts b/src/container/container.ts index 3b7e5b082..50c2b9998 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -16,10 +16,10 @@ import { ModuleActivationStore } from './module_activation_store'; type GetArgs = Omit, 'contextInterceptor' | 'targetType'> -class Container implements interfaces.Container { +class Container implements interfaces.Container { public id: number; - public parent: interfaces.Container | null; + public parent: interfaces.Container

| null; public readonly options: interfaces.ContainerOptions; private _middleware: interfaces.Next | null; private _bindingDictionary: interfaces.Lookup>; @@ -118,10 +118,10 @@ class Container implements interfaces.Container { currentModule.registry( containerModuleHelpers.bindFunction as interfaces.Bind, - containerModuleHelpers.unbindFunction, - containerModuleHelpers.isboundFunction, + containerModuleHelpers.unbindFunction as interfaces.Unbind, + containerModuleHelpers.isboundFunction as interfaces.IsBound, containerModuleHelpers.rebindFunction as interfaces.Rebind, - containerModuleHelpers.unbindAsyncFunction, + containerModuleHelpers.unbindAsyncFunction as interfaces.UnbindAsync, containerModuleHelpers.onActivationFunction as interfaces.Container['onActivation'], containerModuleHelpers.onDeactivationFunction as interfaces.Container['onDeactivation'] ); @@ -140,10 +140,10 @@ class Container implements interfaces.Container { await currentModule.registry( containerModuleHelpers.bindFunction as interfaces.Bind, - containerModuleHelpers.unbindFunction, - containerModuleHelpers.isboundFunction, + containerModuleHelpers.unbindFunction as interfaces.Unbind, + containerModuleHelpers.isboundFunction as interfaces.IsBound, containerModuleHelpers.rebindFunction as interfaces.Rebind, - containerModuleHelpers.unbindAsyncFunction, + containerModuleHelpers.unbindAsyncFunction as interfaces.UnbindAsync, containerModuleHelpers.onActivationFunction as interfaces.Container['onActivation'], containerModuleHelpers.onDeactivationFunction as interfaces.Container['onDeactivation'] ); @@ -173,25 +173,31 @@ class Container implements interfaces.Container { } // Registers a type binding - public bind(serviceIdentifier: interfaces.ServiceIdentifier): interfaces.BindingToSyntax { + public bind, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + ): interfaces.BindingToSyntax { const scope = this.options.defaultScope || BindingScopeEnum.Transient; - const binding = new Binding(serviceIdentifier, scope); + const binding = new Binding(serviceIdentifier as interfaces.ServiceIdentifier, scope); this._bindingDictionary.add(serviceIdentifier, binding as Binding); - return new BindingToSyntax(binding); + return new BindingToSyntax(binding); } - public rebind(serviceIdentifier: interfaces.ServiceIdentifier): interfaces.BindingToSyntax { + public rebind, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + ): interfaces.BindingToSyntax { this.unbind(serviceIdentifier); return this.bind(serviceIdentifier); } - public async rebindAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise> { + public async rebindAsync, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + ): Promise> { await this.unbindAsync(serviceIdentifier); return this.bind(serviceIdentifier); } // Removes a type binding from the registry by its key - public unbind(serviceIdentifier: interfaces.ServiceIdentifier): void { + public unbind>(serviceIdentifier: K): void { if (this._bindingDictionary.hasKey(serviceIdentifier)) { const bindings = this._bindingDictionary.get(serviceIdentifier); @@ -201,7 +207,7 @@ class Container implements interfaces.Container { this._removeServiceFromDictionary(serviceIdentifier); } - public async unbindAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise { + public async unbindAsync>(serviceIdentifier: K): Promise { if (this._bindingDictionary.hasKey(serviceIdentifier)) { const bindings = this._bindingDictionary.get(serviceIdentifier); @@ -232,34 +238,47 @@ class Container implements interfaces.Container { this._bindingDictionary = new Lookup>(); } - public onActivation(serviceIdentifier: interfaces.ServiceIdentifier, onActivation: interfaces.BindingActivation) { + public onActivation, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + onActivation: interfaces.BindingActivation, + ): void { this._activations.add(serviceIdentifier, onActivation as interfaces.BindingActivation); } - public onDeactivation(serviceIdentifier: interfaces.ServiceIdentifier, onDeactivation: interfaces.BindingDeactivation) { + public onDeactivation, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + onDeactivation: interfaces.BindingDeactivation, + ): void { this._deactivations.add(serviceIdentifier, onDeactivation as interfaces.BindingDeactivation); } // Allows to check if there are bindings available for serviceIdentifier - public isBound(serviceIdentifier: interfaces.ServiceIdentifier): boolean { + public isBound>(serviceIdentifier: K): boolean { let bound = this._bindingDictionary.hasKey(serviceIdentifier); if (!bound && this.parent) { - bound = this.parent.isBound(serviceIdentifier); + bound = this.parent.isBound(serviceIdentifier as any); } return bound; } // check binding dependency only in current container - public isCurrentBound(serviceIdentifier: interfaces.ServiceIdentifier): boolean { + public isCurrentBound>(serviceIdentifier: K): boolean { return this._bindingDictionary.hasKey(serviceIdentifier); } - public isBoundNamed(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): boolean { + public isBoundNamed>( + serviceIdentifier: K, + named: string | number | symbol, +): boolean { return this.isBoundTagged(serviceIdentifier, METADATA_KEY.NAMED_TAG, named); } // Check if a binding with a complex constraint is available without throwing a error. Ancestors are also verified. - public isBoundTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: unknown): boolean { + public isBoundTagged>( + serviceIdentifier: K, + key: string | number | symbol, + value: unknown, + ): boolean { let bound = false; // verify if there are bindings available for serviceIdentifier on current binding dictionary @@ -271,7 +290,7 @@ class Container implements interfaces.Container { // verify if there is a parent container that could solve the request if (!bound && this.parent) { - bound = this.parent.isBoundTagged(serviceIdentifier, key, value); + bound = this.parent.isBoundTagged(serviceIdentifier as any, key, value); } return bound; @@ -299,8 +318,8 @@ class Container implements interfaces.Container { this._moduleActivationStore = snapshot.moduleActivationStore } - public createChild(containerOptions?: interfaces.ContainerOptions): Container { - const child = new Container(containerOptions || this.options); + public createChild(containerOptions?: interfaces.ContainerOptions): Container { + const child = new Container(containerOptions || this.options); child.parent = this; return child; } @@ -319,96 +338,122 @@ class Container implements interfaces.Container { // Resolves a dependency by its runtime identifier // The runtime identifier must be associated with only one binding // use getAll when the runtime identifier is associated with multiple bindings - public get(serviceIdentifier: interfaces.ServiceIdentifier): T { + public get, K extends interfaces.ContainerIdentifier = any>(serviceIdentifier: K): B { const getArgs = this._getNotAllArgs(serviceIdentifier, false); - return this._getButThrowIfAsync(getArgs) as T; + return this._getButThrowIfAsync(getArgs) as B; } - public async getAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise { + public async getAsync, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + ): Promise { const getArgs = this._getNotAllArgs(serviceIdentifier, false); - return this._get(getArgs) as Promise | T; + return this._get(getArgs) as Promise | B; } - public getTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: unknown): T { + public getTagged, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + key: string | number | symbol, + value: unknown, + ): B { const getArgs = this._getNotAllArgs(serviceIdentifier, false, key, value); - return this._getButThrowIfAsync(getArgs) as T; + return this._getButThrowIfAsync(getArgs) as B; } - public async getTaggedAsync( - serviceIdentifier: interfaces.ServiceIdentifier, + public async getTaggedAsync, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, key: string | number | symbol, - value: unknown - ): Promise { + value: unknown, + ): Promise { const getArgs = this._getNotAllArgs(serviceIdentifier, false, key, value); - return this._get(getArgs) as Promise | T; + return this._get(getArgs) as Promise | B; } - public getNamed(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): T { - return this.getTagged(serviceIdentifier, METADATA_KEY.NAMED_TAG, named); + public getNamed, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + named: string | number | symbol, + ): B { + return this.getTagged(serviceIdentifier, METADATA_KEY.NAMED_TAG, named); } - public getNamedAsync(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): Promise { - return this.getTaggedAsync(serviceIdentifier, METADATA_KEY.NAMED_TAG, named); + public getNamedAsync, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + named: string | number | symbol, + ): Promise { + return this.getTaggedAsync(serviceIdentifier, METADATA_KEY.NAMED_TAG, named); } // Resolves a dependency by its runtime identifier // The runtime identifier can be associated with one or multiple bindings - public getAll(serviceIdentifier: interfaces.ServiceIdentifier): T[] { + public getAll, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + ): B[] { const getArgs = this._getAllArgs(serviceIdentifier); - return this._getButThrowIfAsync(getArgs) as T[]; + return this._getButThrowIfAsync(getArgs) as B[]; } - public getAllAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise { + public getAllAsync, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + ): Promise { const getArgs = this._getAllArgs(serviceIdentifier); - return this._getAll(getArgs); + return this._getAll(getArgs) as Promise; } - public getAllTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: unknown): T[] { + public getAllTagged, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + key: string | number | symbol, + value: unknown, + ): B[] { const getArgs = this._getNotAllArgs(serviceIdentifier, true, key, value); - return this._getButThrowIfAsync(getArgs) as T[]; + return this._getButThrowIfAsync(getArgs) as B[]; } - public getAllTaggedAsync( - serviceIdentifier: interfaces.ServiceIdentifier, + public getAllTaggedAsync, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, key: string | number | symbol, - value: unknown - ): Promise { + value: unknown, + ): Promise { const getArgs = this._getNotAllArgs(serviceIdentifier, true, key, value); - return this._getAll(getArgs); + return this._getAll(getArgs) as Promise; } - public getAllNamed(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): T[] { - return this.getAllTagged(serviceIdentifier, METADATA_KEY.NAMED_TAG, named); + public getAllNamed, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + named: string | number | symbol, + ): B[] { + return this.getAllTagged(serviceIdentifier, METADATA_KEY.NAMED_TAG, named); } - public getAllNamedAsync(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): Promise { - return this.getAllTaggedAsync(serviceIdentifier, METADATA_KEY.NAMED_TAG, named); + public getAllNamedAsync, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + named: string | number | symbol, + ): Promise { + return this.getAllTaggedAsync(serviceIdentifier, METADATA_KEY.NAMED_TAG, named); } - public resolve(constructorFunction: interfaces.Newable) { - const isBound = this.isBound(constructorFunction); + public resolve(constructorFunction: interfaces.Newable): B { + const isBound = this.isBound(constructorFunction as any); if (!isBound) { - this.bind(constructorFunction).toSelf(); + this.bind(constructorFunction as any).toSelf(); } - const resolved = this.get(constructorFunction); + const resolved = this.get(constructorFunction as any) as B; if (!isBound) { - this.unbind(constructorFunction); + this.unbind(constructorFunction as any); } return resolved; } - private _preDestroy(constructor: NewableFunction, instance: T): Promise | void { + private _preDestroy(constructor: NewableFunction, instance: B): Promise | void { if (Reflect.hasMetadata(METADATA_KEY.PRE_DESTROY, constructor)) { const data: interfaces.Metadata = Reflect.getMetadata(METADATA_KEY.PRE_DESTROY, constructor); - return (instance as interfaces.Instance)[(data.value as string)]?.(); + return (instance as interfaces.Instance)[(data.value as string)]?.(); } } private _removeModuleHandlers(moduleId: number): void { @@ -422,7 +467,7 @@ class Container implements interfaces.Container { return this._bindingDictionary.removeByCondition(binding => binding.moduleId === moduleId); } - private _deactivate(binding: Binding, instance: T): void | Promise { + private _deactivate(binding: Binding, instance: B): void | Promise { const constructor: NewableFunction = Object.getPrototypeOf(instance).constructor; try { @@ -465,8 +510,8 @@ class Container implements interfaces.Container { } - private _deactivateContainer( - instance: T, + private _deactivateContainer( + instance: B, deactivationsIterator: IterableIterator>, ): void | Promise { let deactivation = deactivationsIterator.next(); @@ -484,8 +529,8 @@ class Container implements interfaces.Container { } } - private async _deactivateContainerAsync( - instance: T, + private async _deactivateContainerAsync( + instance: B, deactivationsIterator: IterableIterator>, ): Promise { let deactivation = deactivationsIterator.next(); @@ -505,44 +550,52 @@ class Container implements interfaces.Container { (bindingToSyntax as unknown as { _binding: { moduleId: interfaces.ContainerModuleBase['id'] } } )._binding.moduleId = moduleId; }; - const getBindFunction = (moduleId: interfaces.ContainerModuleBase['id']) => - (serviceIdentifier: interfaces.ServiceIdentifier) => { + const getBindFunction = , K extends interfaces.ContainerIdentifier = any>( + moduleId: interfaces.ContainerModuleBase['id'], + ) => + (serviceIdentifier: K) => { const bindingToSyntax = this.bind(serviceIdentifier); setModuleId(bindingToSyntax, moduleId); - return bindingToSyntax as BindingToSyntax; + return bindingToSyntax as BindingToSyntax; }; - const getUnbindFunction = () => - (serviceIdentifier: interfaces.ServiceIdentifier) => { + const getUnbindFunction = >() => + (serviceIdentifier: K) => { return this.unbind(serviceIdentifier); }; - const getUnbindAsyncFunction = () => - (serviceIdentifier: interfaces.ServiceIdentifier) => { + const getUnbindAsyncFunction = >() => + (serviceIdentifier: K) => { return this.unbindAsync(serviceIdentifier); }; - const getIsboundFunction = () => - (serviceIdentifier: interfaces.ServiceIdentifier) => { + const getIsboundFunction = >() => + (serviceIdentifier: K) => { return this.isBound(serviceIdentifier) }; - const getRebindFunction = (moduleId: interfaces.ContainerModuleBase['id']) => - (serviceIdentifier: interfaces.ServiceIdentifier) => { + const getRebindFunction = , K extends interfaces.ContainerIdentifier = any>( + moduleId: interfaces.ContainerModuleBase['id'], + ) => + (serviceIdentifier: K) => { const bindingToSyntax = this.rebind(serviceIdentifier); setModuleId(bindingToSyntax, moduleId); - return bindingToSyntax as BindingToSyntax; + return bindingToSyntax as BindingToSyntax; }; - const getOnActivationFunction = (moduleId: interfaces.ContainerModuleBase['id']) => - (serviceIdentifier: interfaces.ServiceIdentifier, onActivation: interfaces.BindingActivation) => { - this._moduleActivationStore.addActivation(moduleId, serviceIdentifier, onActivation); + const getOnActivationFunction = , K extends interfaces.ContainerIdentifier = any>( + moduleId: interfaces.ContainerModuleBase['id'], + ) => + (serviceIdentifier: K, onActivation: interfaces.BindingActivation) => { + this._moduleActivationStore.addActivation(moduleId, serviceIdentifier, onActivation as interfaces.BindingActivation); this.onActivation(serviceIdentifier, onActivation); } - const getOnDeactivationFunction = (moduleId: interfaces.ContainerModuleBase['id']) => - (serviceIdentifier: interfaces.ServiceIdentifier, onDeactivation: interfaces.BindingDeactivation) => { - this._moduleActivationStore.addDeactivation(moduleId, serviceIdentifier, onDeactivation); + const getOnDeactivationFunction = , K extends interfaces.ContainerIdentifier = any>( + moduleId: interfaces.ContainerModuleBase['id'], + ) => + (serviceIdentifier: K, onDeactivation: interfaces.BindingDeactivation) => { + this._moduleActivationStore.addDeactivation(moduleId, serviceIdentifier, onDeactivation as interfaces.BindingDeactivation); this.onDeactivation(serviceIdentifier, onDeactivation); } @@ -557,14 +610,18 @@ class Container implements interfaces.Container { }); } - private _getAll(getArgs: GetArgs): Promise { - return Promise.all(this._get(getArgs) as (Promise | T)[]); + private _getAll, K extends interfaces.ContainerIdentifier = any>( + getArgs: GetArgs, + ): Promise { + return Promise.all(this._get(getArgs) as (Promise | B)[]); } // Prepares arguments required for resolution and // delegates resolution to _middleware if available // otherwise it delegates resolution to _planAndResolve - private _get(getArgs: GetArgs): interfaces.ContainerResolution { - const planAndResolveArgs: interfaces.NextArgs = { + private _get, K extends interfaces.ContainerIdentifier = any>( + getArgs: GetArgs, + ): interfaces.ContainerResolution { + const planAndResolveArgs: interfaces.NextArgs = { ...getArgs, contextInterceptor: (context) => context, targetType: TargetTypeEnum.Variable @@ -574,44 +631,46 @@ class Container implements interfaces.Container { if (middlewareResult === undefined || middlewareResult === null) { throw new Error(ERROR_MSGS.INVALID_MIDDLEWARE_RETURN); } - return middlewareResult as interfaces.ContainerResolution; + return middlewareResult as interfaces.ContainerResolution; } - return this._planAndResolve()(planAndResolveArgs); + return this._planAndResolve()(planAndResolveArgs); } - private _getButThrowIfAsync( - getArgs: GetArgs, - ): (T | T[]) { - const result = this._get(getArgs); + private _getButThrowIfAsync, K extends interfaces.ContainerIdentifier = any>( + getArgs: GetArgs, + ): (B | B[]) { + const result = this._get(getArgs); - if (isPromiseOrContainsPromise(result)) { + if (isPromiseOrContainsPromise(result)) { throw new Error(ERROR_MSGS.LAZY_IN_SYNC(getArgs.serviceIdentifier)); } - return result as (T | T[]); + return result as (B | B[]); } - private _getAllArgs(serviceIdentifier: interfaces.ServiceIdentifier): GetArgs { - const getAllArgs: GetArgs = { + private _getAllArgs, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + ): GetArgs { + const getAllArgs: GetArgs = { avoidConstraints: true, isMultiInject: true, - serviceIdentifier, + serviceIdentifier: serviceIdentifier as interfaces.ServiceIdentifier, }; return getAllArgs; } - private _getNotAllArgs( - serviceIdentifier: interfaces.ServiceIdentifier, + private _getNotAllArgs, K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, isMultiInject: boolean, key?: string | number | symbol | undefined, value?: unknown, - ): GetArgs { - const getNotAllArgs: GetArgs = { + ): GetArgs { + const getNotAllArgs: GetArgs = { avoidConstraints: false, isMultiInject, - serviceIdentifier, + serviceIdentifier: serviceIdentifier as interfaces.ServiceIdentifier, key, value, }; @@ -622,8 +681,8 @@ class Container implements interfaces.Container { // Planner creates a plan and Resolver resolves a plan // one of the jobs of the Container is to links the Planner // with the Resolver and that is what this function is about - private _planAndResolve(): (args: interfaces.NextArgs) => interfaces.ContainerResolution { - return (args: interfaces.NextArgs) => { + private _planAndResolve(): (args: interfaces.NextArgs) => interfaces.ContainerResolution { + return (args: interfaces.NextArgs) => { // create a plan let context = plan( @@ -641,7 +700,7 @@ class Container implements interfaces.Container { context = args.contextInterceptor(context); // resolve plan - const result = resolve(context); + const result = resolve(context); return result; @@ -674,9 +733,9 @@ class Container implements interfaces.Container { await Promise.all(bindings.map(b => this._deactivateIfSingleton(b))) } - private _propagateContainerDeactivationThenBindingAndPreDestroy( - binding: Binding, - instance: T, + private _propagateContainerDeactivationThenBindingAndPreDestroy( + binding: Binding, + instance: B, constructor: NewableFunction ): void | Promise { if (this.parent) { @@ -686,9 +745,9 @@ class Container implements interfaces.Container { } } - private async _propagateContainerDeactivationThenBindingAndPreDestroyAsync( - binding: Binding, - instance: T, + private async _propagateContainerDeactivationThenBindingAndPreDestroyAsync( + binding: Binding, + instance: B, constructor: NewableFunction ): Promise { if (this.parent) { @@ -706,9 +765,9 @@ class Container implements interfaces.Container { } } - private _bindingDeactivationAndPreDestroy( - binding: Binding, - instance: T, + private _bindingDeactivationAndPreDestroy( + binding: Binding, + instance: B, constructor: NewableFunction ): void | Promise { if (typeof binding.onDeactivation === 'function') { @@ -722,9 +781,9 @@ class Container implements interfaces.Container { return this._preDestroy(constructor, instance); } - private async _bindingDeactivationAndPreDestroyAsync( - binding: Binding, - instance: T, + private async _bindingDeactivationAndPreDestroyAsync( + binding: Binding, + instance: B, constructor: NewableFunction ): Promise { if (typeof binding.onDeactivation === 'function') { diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index 773833c1d..933468eca 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -46,7 +46,16 @@ namespace interfaces { prototype: T; } - export type ServiceIdentifier = (string | symbol | Newable | Abstract); + export type IfAny = 0 extends (1 & T) ? Y : N; + + export type PropertyServiceIdentifier = (string | symbol); + export type ServiceIdentifier = (PropertyServiceIdentifier | Newable | Abstract); + export type BindingMap = Record; + export type BindingMapKey = keyof T & PropertyServiceIdentifier; + export type ContainerIdentifier = IfAny>; + export type ContainerBinding = any> = K extends keyof T ? T[K] : + K extends Newable ? C : + K extends Abstract ? D : never; export interface Clonable { clone(): T; @@ -188,36 +197,53 @@ namespace interfaces { skipBaseClassChecks?: boolean; } - export interface Container { + export interface Container { id: number; - parent: Container | null; + parent: Container

| null; options: ContainerOptions; - bind(serviceIdentifier: ServiceIdentifier): BindingToSyntax; - rebind(serviceIdentifier: interfaces.ServiceIdentifier): interfaces.BindingToSyntax; - rebindAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise> - unbind(serviceIdentifier: ServiceIdentifier): void; - unbindAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise; + bind: Bind; + rebind: Rebind; + rebindAsync: RebindAsync; + unbind: Unbind; + unbindAsync: UnbindAsync; unbindAll(): void; unbindAllAsync(): Promise; - isBound(serviceIdentifier: ServiceIdentifier): boolean; - isCurrentBound(serviceIdentifier: ServiceIdentifier): boolean; - isBoundNamed(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): boolean; - isBoundTagged(serviceIdentifier: ServiceIdentifier, key: string | number | symbol, value: unknown): boolean; - get(serviceIdentifier: ServiceIdentifier): T; - getNamed(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): T; - getTagged(serviceIdentifier: ServiceIdentifier, key: string | number | symbol, value: unknown): T; - getAll(serviceIdentifier: ServiceIdentifier): T[]; - getAllTagged(serviceIdentifier: ServiceIdentifier, key: string | number | symbol, value: unknown): T[]; - getAllNamed(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): T[]; - getAsync(serviceIdentifier: ServiceIdentifier): Promise; - getNamedAsync(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): Promise; - getTaggedAsync(serviceIdentifier: ServiceIdentifier, key: string | number | symbol, value: unknown): Promise; - getAllAsync(serviceIdentifier: ServiceIdentifier): Promise; - getAllTaggedAsync(serviceIdentifier: ServiceIdentifier, key: string | number | symbol, value: unknown): Promise; - getAllNamedAsync(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): Promise; - onActivation(serviceIdentifier: ServiceIdentifier, onActivation: BindingActivation): void; - onDeactivation(serviceIdentifier: ServiceIdentifier, onDeactivation: BindingDeactivation): void; - resolve(constructorFunction: interfaces.Newable): T; + isBound: IsBound; + isCurrentBound: IsBound; + isBoundNamed: >(serviceIdentifier: K, named: PropertyKey) => boolean; + isBoundTagged: >( + serviceIdentifier: K, + key: PropertyKey, + value: unknown, + ) => boolean; + get: Get; + getNamed: , K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + named: PropertyKey, + ) => B; + getTagged: , K extends interfaces.ContainerIdentifier = any>( + serviceIdentifier: K, + key: PropertyKey, + value: unknown, + ) => B; + getAll: All>; + getAllTagged: All['getTagged']>; + getAllNamed: All['getNamed']>; + getAsync: Async>; + getNamedAsync: Async['getNamed']>; + getTaggedAsync: Async['getTagged']>; + getAllAsync: Async>>; + getAllTaggedAsync: Async['getTagged']>>; + getAllNamedAsync: Async['getNamed']>>; + onActivation, K extends ContainerIdentifier = any>( + serviceIdentifier: K, + onActivation: BindingActivation, + ): void; + onDeactivation, K extends ContainerIdentifier = any>( + serviceIdentifier: K, + onDeactivation: BindingDeactivation, + ): void; + resolve(constructorFunction: Newable): B; load(...modules: ContainerModule[]): void; loadAsync(...modules: AsyncContainerModule[]): Promise; unload(...modules: ContainerModuleBase[]): void; @@ -226,18 +252,27 @@ namespace interfaces { applyMiddleware(...middleware: Middleware[]): void; snapshot(): void; restore(): void; - createChild(): Container; + createChild(containerOptions?: ContainerOptions): Container; } - export type Bind = (serviceIdentifier: ServiceIdentifier) => BindingToSyntax; + type Async any> = (...args: Parameters) => Promise>; + type All any> = (...args: Parameters) => ReturnType[]; + + export type Bind = + , K extends ContainerIdentifier = any>(serviceIdentifier: K) => BindingToSyntax; + + export type Rebind = Bind; + + export type RebindAsync = Async>; - export type Rebind = (serviceIdentifier: ServiceIdentifier) => BindingToSyntax; + export type Unbind = >(serviceIdentifier: K) => void; - export type Unbind = (serviceIdentifier: ServiceIdentifier) => void; + export type UnbindAsync = Async>; - export type UnbindAsync = (serviceIdentifier: ServiceIdentifier) => Promise; + export type IsBound = >(serviceIdentifier: K) => boolean; - export type IsBound = (serviceIdentifier: ServiceIdentifier) => boolean; + type Get = + , K extends ContainerIdentifier = any>(serviceIdentifier: K) => B; export interface ContainerModuleBase { id: number; diff --git a/src/planning/planner.ts b/src/planning/planner.ts index c995f6180..69cea3db3 100644 --- a/src/planning/planner.ts +++ b/src/planning/planner.ts @@ -217,9 +217,9 @@ function getBindings( return bindings; } -function plan( +function plan( metadataReader: interfaces.MetadataReader, - container: interfaces.Container, + container: interfaces.Container, isMultiInject: boolean, targetType: interfaces.TargetType, serviceIdentifier: interfaces.ServiceIdentifier, @@ -245,8 +245,8 @@ function plan( } -function createMockRequest( - container: interfaces.Container, +function createMockRequest( + container: interfaces.Container, serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: unknown diff --git a/src/syntax/binding_to_syntax.ts b/src/syntax/binding_to_syntax.ts index cfcf9ca95..792ab8e71 100644 --- a/src/syntax/binding_to_syntax.ts +++ b/src/syntax/binding_to_syntax.ts @@ -82,7 +82,7 @@ class BindingToSyntax implements interfaces.BindingToSyntax { public toAutoNamedFactory(serviceIdentifier: interfaces.ServiceIdentifier): BindingWhenOnSyntax { this._binding.type = BindingTypeEnum.Factory; this._binding.factory = (context) => { - return (named: unknown) => context.container.getNamed(serviceIdentifier, named as string); + return (named: unknown) => context.container.getNamed(serviceIdentifier, named as string); }; return new BindingWhenOnSyntax(this._binding); } diff --git a/test/interfaces/interfaces.test.ts b/test/interfaces/interfaces.test.ts new file mode 100644 index 000000000..daee02739 --- /dev/null +++ b/test/interfaces/interfaces.test.ts @@ -0,0 +1,108 @@ +import { interfaces } from '../../src/interfaces/interfaces'; +import {Container, injectable} from '../../src/inversify'; +import {expect} from 'chai'; + +describe('interfaces', () => { + @injectable() + class Foo { + foo: string = ''; + } + + @injectable() + class Bar { + bar: string = ''; + } + + describe('Container', () => { + describe('no binding map', () => { + let container: interfaces.Container; + let foo: Foo; + + beforeEach(() => { + container = new Container(); + // tslint:disable-next-line: no-unused-expression + foo; + }); + + describe('bind()', () => { + it('binds without a type argument', () => { + container.bind('foo').to(Foo); + container.bind(Foo).to(Foo); + }); + + it('checks bindings with an explicit type argument', () => { + container.bind('foo').to(Foo); + // @ts-expect-error :: can't bind Bar to Foo + container.bind('foo').to(Bar); + }); + + it('binds a class as a service identifier', () => { + container.bind(Foo).to(Foo); + // @ts-expect-error :: can't bind Bar to Foo + container.bind(Foo).to(Bar); + }); + }); + + describe('get()', () => { + beforeEach(() => { + container.bind('foo').to(Foo); + container.bind('bar').to(Bar); + container.bind(Foo).to(Foo); + container.bind(Bar).to(Bar); + }); + + it('gets an anonymous binding', () => { + foo = container.get('foo'); + }); + + it('enforces type arguments', () => { + foo = container.get('foo'); + // @ts-expect-error :: can't assign Bar to Foo + foo = container.get('bar'); + }); + + it('gets a class identifier', () => { + foo = container.get(Foo); + // @ts-expect-error :: can't assign Bar to Foo + foo = container.get(Bar); + }); + }); + }); + + describe('binding map', () => { + let container: interfaces.Container<{foo: Foo; bar: Bar}>; + let foo: Foo; + + beforeEach(() => { + container = new Container(); + // tslint:disable-next-line: no-unused-expression + foo; + }); + + describe('bind()', () => { + it('enforces strict bindings', () => { + container.bind('foo').to(Foo); + // @ts-expect-error :: can't bind Bar to Foo + container.bind('foo').to(Bar); + // @ts-expect-error :: unknown service identifier + container.bind('unknown').to(Foo); + }); + }); + + describe('get()', () => { + beforeEach(() => { + container.bind('foo').to(Foo); + container.bind('bar').to(Bar); + }); + + it('enforces strict bindings', () => { + foo = container.get('foo'); + // @ts-expect-error :: can't assign Bar to Foo + foo = container.get('bar'); + // @ts-expect-error :: unknown service identifier + expect(() => container.get('unknown')).to.throw('No matching bindings'); + }); + }); + }); + }); +}); diff --git a/wiki/container_typing.md b/wiki/container_typing.md new file mode 100644 index 000000000..e9f61b583 --- /dev/null +++ b/wiki/container_typing.md @@ -0,0 +1,21 @@ +# Strong Container typing + +The `Container` can take an optional type argument defining a mapping of service identifiers to types. If defined, this +will add strong type-checking when declaring bindings, and when retrieving them. + +For example: + +```ts +type IdentifierMap = { + foo: Foo; + bar: Bar; +}; + +const container = new Container; + +container.bind('foo').to(Foo); // ok +container.bind('foo').to(Bar); // error + +const foo: Foo = container.get('foo') // ok +const bar: Bar = container.get('foo') // error +``` diff --git a/wiki/readme.md b/wiki/readme.md index c37cf2d33..311d8337d 100644 --- a/wiki/readme.md +++ b/wiki/readme.md @@ -17,6 +17,7 @@ Welcome to the InversifyJS wiki! - [Container API](./container_api.md) - [Declaring container modules](./container_modules.md) - [Container snapshots](./container_snapshots.md) +- [Strong Container typing](./container_typing.md) - [Controlling the scope of the dependencies](./scope.md) - [Declaring optional dependencies](./optional_dependencies.md) - [Injecting a constant or dynamic value](./value_injection.md)