From 8e699915910af254593e6862695142c57fb386b5 Mon Sep 17 00:00:00 2001 From: Mikalai Silivonik Date: Wed, 27 Nov 2024 15:54:37 -0500 Subject: [PATCH 01/14] fix providesClass --- src/Container.ts | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Container.ts b/src/Container.ts index a96d7c6..98ac99b 100644 --- a/src/Container.ts +++ b/src/Container.ts @@ -1,8 +1,18 @@ import type { Memoized } from "./memoize"; import { isMemoized, memoize } from "./memoize"; import { PartialContainer } from "./PartialContainer"; -import type { AddService, AddServices, InjectableClass, InjectableFunction, TokenType, ValidTokens } from "./types"; -import { ClassInjectable, ConcatInjectable, Injectable } from "./Injectable"; +import type { + AddService, + AddServices, + AsTuple, + CorrespondingServices, + InjectableClass, + InjectableFunction, + ServicesFromTokenizedParams, + TokenType, + ValidTokens, +} from "./types"; +import { ConcatInjectable, Injectable } from "./Injectable"; import { entries } from "./entries"; type MaybeMemoizedFactories = { @@ -418,10 +428,19 @@ export class Container { * specifying these dependencies. * @returns A new Container instance containing the newly created service, allowing for method chaining. */ - providesClass = []>( + providesClass = < + Token extends TokenType, + Tokens extends readonly ValidTokens[], + Class extends { + readonly dependencies: Tokens; + new (...args: Params): InstanceType; + }, + Params extends AsTuple>, + >( token: Token, - cls: InjectableClass - ) => this.providesService(ClassInjectable(token, cls)); + cls: Class + ): Container>> => + this.providesService(Injectable(token, cls.dependencies, (...args: Params) => new cls(...args))); /** * Registers a static value as a service in the container. This method is ideal for services that do not From 40342fb51452a6bb7c383ac9d2c3d4ac7357cd74 Mon Sep 17 00:00:00 2001 From: Mikalai Silivonik Date: Wed, 27 Nov 2024 21:59:54 -0500 Subject: [PATCH 02/14] more --- src/Container.ts | 70 ++++++++++++++++++++------------ src/Injectable.ts | 23 ++++++----- src/__tests__/Injectable.spec.ts | 19 ++++++++- src/types.ts | 6 ++- 4 files changed, 79 insertions(+), 39 deletions(-) diff --git a/src/Container.ts b/src/Container.ts index 98ac99b..e82a7c9 100644 --- a/src/Container.ts +++ b/src/Container.ts @@ -4,13 +4,11 @@ import { PartialContainer } from "./PartialContainer"; import type { AddService, AddServices, - AsTuple, - CorrespondingServices, - InjectableClass, + MapTokensToTypes, InjectableFunction, - ServicesFromTokenizedParams, TokenType, ValidTokens, + ServicesFromTokenizedParams, } from "./types"; import { ConcatInjectable, Injectable } from "./Injectable"; import { entries } from "./entries"; @@ -287,7 +285,7 @@ export class Container { * in the provided {@link PartialContainer} initialized as needed. */ run( - // FullfilledDependencies is assignable to Dependencies -- by specifying Container as the + // FulfilledDependencies is assignable to Dependencies -- by specifying Container as the // `this` type, we ensure this Container can provide all the Dependencies required by the PartialContainer. this: Container, container: PartialContainer @@ -350,7 +348,7 @@ export class Container { * `PartialContainer`, with services from the `PartialContainer` taking precedence in case of conflicts. */ provides( - // FullfilledDependencies is assignable to Dependencies -- by specifying Container as the + // FulfilledDependencies is assignable to Dependencies -- by specifying Container as the // `this` type, we ensure this Container can provide all the Dependencies required by the PartialContainer. this: Container, container: PartialContainer @@ -435,7 +433,7 @@ export class Container { readonly dependencies: Tokens; new (...args: Params): InstanceType; }, - Params extends AsTuple>, + Params extends MapTokensToTypes, >( token: Token, cls: Class @@ -452,8 +450,10 @@ export class Container { * @returns A new Container instance that includes the provided service, allowing for chaining additional * `provides` calls. */ - providesValue = (token: Token, value: Service) => - this.providesService(Injectable(token, [], () => value)); + providesValue = ( + token: Token, + value: Service + ): Container> => this.providesService(Injectable(token, [], () => value)); /** * Appends a value to the array associated with a specified token in the current Container, then returns @@ -471,10 +471,11 @@ export class Container { * @param value - A value to append to the array. * @returns The updated Container with the appended value in the specified array. */ - appendValue = >( + appendValue = ( token: Token, - value: Service - ) => this.providesService(ConcatInjectable(token, () => value)) as Container; + value: ArrayElement + ): Container[]>> => + this.providesService(ConcatInjectable(token, () => value)); /** * Appends an injectable class factory to the array associated with a specified token in the current Container, @@ -494,14 +495,16 @@ export class Container { appendClass = < Token extends keyof Services, Tokens extends readonly ValidTokens[], - Service extends ArrayElement, + Class extends { + readonly dependencies: Tokens; + new (...args: Params): InstanceType; + }, + Params extends MapTokensToTypes, >( token: Token, - cls: InjectableClass - ) => - this.providesService( - ConcatInjectable(token, () => this.providesClass(token, cls).get(token)) - ) as Container; + cls: Class + ): Container[]>> => + this.providesService(ConcatInjectable(token, cls.dependencies, (...args: Params) => new cls(...args))); /** * Appends a new service instance to an existing array within the container using an `InjectableFunction`. @@ -521,15 +524,30 @@ export class Container { * specified by the token. */ append = < - Token extends keyof Services, - Tokens extends readonly ValidTokens[], - Service extends ArrayElement, + Token extends keyof Services, + Tokens extends readonly ValidTokens[], + // Fn extends { + // (...args: Params): ArrayElement; + // token: Token; + // dependencies: Tokens; + // }, + Fn extends InjectableFunction>, + Params extends MapTokensToTypes, + Deps extends ServicesFromTokenizedParams >( - fn: InjectableFunction - ) => - this.providesService( - ConcatInjectable(fn.token, () => this.providesService(fn).get(fn.token)) - ) as Container; + fn: Fn + ): Container[]>> => { + type ee = Fn["dependencies"]; + const i = ConcatInjectable< + Token, + Tokens, + Params, + ArrayElement[], + Deps + >(fn.token, fn.dependencies, (...args) => fn(...args)); + const p = this.providesService(i); + return p; + }; private providesService< Token extends TokenType, diff --git a/src/Injectable.ts b/src/Injectable.ts index 2fecce9..4848536 100644 --- a/src/Injectable.ts +++ b/src/Injectable.ts @@ -56,18 +56,17 @@ export function Injectable( export function Injectable< Token extends TokenType, const Tokens extends readonly TokenType[], - Params extends readonly any[], + Params extends readonly any[] & { length: Tokens["length"] }, Service, + Deps extends ServicesFromTokenizedParams, >( token: Token, dependencies: Tokens, // The function arity (number of arguments) must match the number of dependencies specified – if they don't, we'll // force a compiler error by saying the arguments should be `void[]`. We'll also throw at runtime, so the return // type will be `never`. - fn: (...args: Tokens["length"] extends Params["length"] ? Params : void[]) => Service -): Tokens["length"] extends Params["length"] - ? InjectableFunction, Tokens, Token, Service> - : never; + fn: (...args: Params) => Service +): InjectableFunction; export function Injectable( token: TokenType, @@ -79,11 +78,12 @@ export function Injectable( if (!fn) { throw new TypeError( - "[Injectable] Received invalid arguments. The factory function must be either the second " + "or third argument." + "[Injectable] Received invalid arguments. The factory function must be either the second or third argument." ); } - if (fn.length !== dependencies.length) { + // const length = actualLength in fn ? fn[actualLength] as number : fn.length; + if (fn.length !== 0 && fn.length !== dependencies.length) { throw new TypeError( "[Injectable] Function arity does not match the number of dependencies. Function has arity " + `${fn.length}, but ${dependencies.length} dependencies were specified.` + @@ -219,12 +219,13 @@ export function ConcatInjectable( export function ConcatInjectable< Token extends TokenType, const Tokens extends readonly TokenType[], - Params extends readonly any[], + Params extends readonly any[] & { length: Tokens["length"] }, Service, + Deps extends ServicesFromTokenizedParams, >( token: Token, dependencies: Tokens, - fn: (...args: Tokens["length"] extends Params["length"] ? Params : void[]) => Service + fn: (...args: Params) => Service ): InjectableFunction, Tokens, Token, Service[]>; export function ConcatInjectable( @@ -242,9 +243,9 @@ export function ConcatInjectable( ); } - if (fn.length !== dependencies.length) { + if (fn.length !== 0 && fn.length !== dependencies.length) { throw new TypeError( - "[Injectable] Function arity does not match the number of dependencies. Function has arity " + + "[ConcatInjectable] Function arity does not match the number of dependencies. Function has arity " + `${fn.length}, but ${dependencies.length} dependencies were specified.` + `\nDependencies: ${JSON.stringify(dependencies)}` ); diff --git a/src/__tests__/Injectable.spec.ts b/src/__tests__/Injectable.spec.ts index 65febad..0cf4a7c 100644 --- a/src/__tests__/Injectable.spec.ts +++ b/src/__tests__/Injectable.spec.ts @@ -1,6 +1,18 @@ import { Injectable } from "../Injectable"; describe("Injectable", () => { + test("is created with not dependencies specified", () => { + expect(() => Injectable("TestService", () => {})).not.toThrow(); + }); + + test("is created with empty array specified", () => { + expect(() => Injectable("TestService", [], () => {})).not.toThrow(); + }); + + test("is created with dependency specified", () => { + expect(() => Injectable("TestService", ["a"], (_a: number) => {})).not.toThrow(); + }); + describe("when given invalid arguments", () => { test("a TypeError is thrown", () => { expect(() => Injectable("TestService", [] as any)).toThrowError(TypeError); @@ -8,8 +20,13 @@ describe("Injectable", () => { }); describe("when given a function with arity unequal to the number of dependencies", () => { - test("a TypeError is thrown", () => { + test("a compilation error is thrown", () => { + // @ts-expect-error must fail to compile as the factory function arity doesn't match dependencies array expect(() => Injectable("TestService", [] as const, (_: any) => {})).toThrowError(TypeError); }); + + test("a TypeError is thrown", () => { + expect(() => Injectable("TestService", [] as const, ((_: any) => {}) as any)).toThrowError(TypeError); + }); }); }); diff --git a/src/types.ts b/src/types.ts index d29a561..294a97a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,12 +43,16 @@ export type InjectableFunction< Service, > = Tokens extends readonly ValidTokens[] ? { - (...args: AsTuple>): Service; + (...args: MapTokensToTypes): Service; token: Token; dependencies: Tokens; } : never; +export type MapTokensToTypes[]> = AsTuple< + CorrespondingServices +>; + /** * Represents a class that can be used as an injectable service within a dependency injection {@link Container}. * The `InjectableClass` type ensures that the class's dependencies and constructor signature align with From 1479ef26385afaa66861cd9d845c5d6d4a8151b2 Mon Sep 17 00:00:00 2001 From: Mikalai Silivonik Date: Wed, 27 Nov 2024 22:20:15 -0500 Subject: [PATCH 03/14] fixed append --- src/Container.ts | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/Container.ts b/src/Container.ts index e82a7c9..42f15f2 100644 --- a/src/Container.ts +++ b/src/Container.ts @@ -524,30 +524,18 @@ export class Container { * specified by the token. */ append = < - Token extends keyof Services, - Tokens extends readonly ValidTokens[], - // Fn extends { - // (...args: Params): ArrayElement; - // token: Token; - // dependencies: Tokens; - // }, - Fn extends InjectableFunction>, - Params extends MapTokensToTypes, - Deps extends ServicesFromTokenizedParams + Token extends keyof Services, + Tokens extends readonly ValidTokens[], + Fn extends { + (...args: Params): ArrayElement; + token: Token; + dependencies: Tokens; + }, + Params extends MapTokensToTypes, >( - fn: Fn - ): Container[]>> => { - type ee = Fn["dependencies"]; - const i = ConcatInjectable< - Token, - Tokens, - Params, - ArrayElement[], - Deps - >(fn.token, fn.dependencies, (...args) => fn(...args)); - const p = this.providesService(i); - return p; - }; + fn: Tokens extends readonly TokenType[] ? Fn : never + ): Container[]>> => + this.providesService(ConcatInjectable(fn.token, fn.dependencies, (...args: Params) => fn(...args))); private providesService< Token extends TokenType, From 3b333d1327cc2e0d7feca93ab1a694292e763bc9 Mon Sep 17 00:00:00 2001 From: Mikalai Silivonik Date: Wed, 27 Nov 2024 22:28:43 -0500 Subject: [PATCH 04/14] fixed fromObject --- src/Container.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/Container.ts b/src/Container.ts index 42f15f2..071c63e 100644 --- a/src/Container.ts +++ b/src/Container.ts @@ -1,17 +1,8 @@ import type { Memoized } from "./memoize"; import { isMemoized, memoize } from "./memoize"; import { PartialContainer } from "./PartialContainer"; -import type { - AddService, - AddServices, - MapTokensToTypes, - InjectableFunction, - TokenType, - ValidTokens, - ServicesFromTokenizedParams, -} from "./types"; +import type { AddService, AddServices, MapTokensToTypes, InjectableFunction, TokenType, ValidTokens } from "./types"; import { ConcatInjectable, Injectable } from "./Injectable"; -import { entries } from "./entries"; type MaybeMemoizedFactories = { [K in keyof Services]: (() => Services[K]) | Memoized<() => Services[K]>; @@ -150,8 +141,8 @@ export class Container { * defines the initial set of services to be contained within the new Container instance. * @returns A new Container instance populated with the provided services. */ - static fromObject(services: Services): Container { - return entries(services).reduce( + static fromObject(services: Services): Container { + return Object.entries(services).reduce( (container, [token, value]) => container.providesValue(token, value), new Container({}) ) as Container; From ee43bb233994b73f9d687979b905c143c0e5ca36 Mon Sep 17 00:00:00 2001 From: Mikalai Silivonik Date: Wed, 27 Nov 2024 22:42:04 -0500 Subject: [PATCH 05/14] cleanup --- src/Injectable.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Injectable.ts b/src/Injectable.ts index 4848536..e718c16 100644 --- a/src/Injectable.ts +++ b/src/Injectable.ts @@ -56,15 +56,13 @@ export function Injectable( export function Injectable< Token extends TokenType, const Tokens extends readonly TokenType[], + // The function arity (number of arguments) must match the number of dependencies specified Params extends readonly any[] & { length: Tokens["length"] }, Service, Deps extends ServicesFromTokenizedParams, >( token: Token, dependencies: Tokens, - // The function arity (number of arguments) must match the number of dependencies specified – if they don't, we'll - // force a compiler error by saying the arguments should be `void[]`. We'll also throw at runtime, so the return - // type will be `never`. fn: (...args: Params) => Service ): InjectableFunction; From b3540c09413e26dc329f99c87f23be268c2126f5 Mon Sep 17 00:00:00 2001 From: Mikalai Silivonik Date: Thu, 28 Nov 2024 09:42:15 -0500 Subject: [PATCH 06/14] simplified (no InstanceType) --- src/Container.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Container.ts b/src/Container.ts index 071c63e..9e58fc9 100644 --- a/src/Container.ts +++ b/src/Container.ts @@ -420,15 +420,15 @@ export class Container { providesClass = < Token extends TokenType, Tokens extends readonly ValidTokens[], - Class extends { - readonly dependencies: Tokens; - new (...args: Params): InstanceType; - }, - Params extends MapTokensToTypes, + Params extends MapTokensToTypes, + Service >( token: Token, - cls: Class - ): Container>> => + cls: { + readonly dependencies: Tokens; + new (...args: Params): Service; + } + ): Container> => this.providesService(Injectable(token, cls.dependencies, (...args: Params) => new cls(...args))); /** @@ -486,15 +486,15 @@ export class Container { appendClass = < Token extends keyof Services, Tokens extends readonly ValidTokens[], - Class extends { - readonly dependencies: Tokens; - new (...args: Params): InstanceType; - }, - Params extends MapTokensToTypes, + Params extends MapTokensToTypes, + Service >( token: Token, - cls: Class - ): Container[]>> => + cls: { + readonly dependencies: Tokens; + new (...args: Params): Service; + } + ): Container> => this.providesService(ConcatInjectable(token, cls.dependencies, (...args: Params) => new cls(...args))); /** From d8df84072fd335e564201cf856329774f8445475 Mon Sep 17 00:00:00 2001 From: Mikalai Silivonik Date: Thu, 28 Nov 2024 11:16:28 -0500 Subject: [PATCH 07/14] fixed partial container --- src/PartialContainer.ts | 54 +++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/src/PartialContainer.ts b/src/PartialContainer.ts index ab69297..955f2b1 100644 --- a/src/PartialContainer.ts +++ b/src/PartialContainer.ts @@ -2,16 +2,8 @@ import { entries } from "./entries"; import type { Memoized } from "./memoize"; import { memoize } from "./memoize"; import type { Container } from "./Container"; -import type { - AddService, - InjectableClass, - InjectableFunction, - ServicesFromTokenizedParams, - TokenType, - ValidTokens, -} from "./types"; -import type { ConstructorReturnType } from "./Injectable"; -import { ClassInjectable, Injectable } from "./Injectable"; +import type { AddService, InjectableFunction, ServicesFromTokenizedParams, TokenType, ValidTokens } from "./types"; +import { Injectable } from "./Injectable"; // Using a conditional type forces TS language services to evaluate the type -- so when showing e.g. type hints, we // will see the mapped type instead of the AddDependencies type alias. This produces better hints. @@ -26,6 +18,13 @@ type AddDependencies = ParentDependencies exte } : never; +type UpdateDependencies< + ExistingServices, + ExistingDependencies, + NewToken extends TokenType, + NewDependencies, +> = AddDependencies, ExcludeKey>; + type ExcludeKey = T extends any ? { [K in Exclude]: T[K] } : never; type PartialInjectableFunction< @@ -101,13 +100,7 @@ export class PartialContainer { fn: PartialInjectableFunction ): PartialContainer< AddService, - // The dependencies of the new PartialContainer are the combined dependencies of this container and the - // PartialInjectableFunction -- but we exclude any dependencies already provided by this container (i.e. this - // container's Services) as well as the new Service being provided. - ExcludeKey< - AddDependencies, ServicesFromTokenizedParams>, - keyof Services - > + UpdateDependencies> > { return new PartialContainer({ ...this.injectables, [fn.token]: fn } as any); } @@ -125,7 +118,10 @@ export class PartialContainer { * @param token the Token by which the value will be known. * @param value the value to be provided. */ - providesValue = (token: Token, value: Service) => + providesValue = ( + token: Token, + value: Service + ): PartialContainer, UpdateDependencies> => this.provides(Injectable(token, [], () => value)); /** @@ -148,15 +144,25 @@ export class PartialContainer { * @param cls the class to be provided, must match the InjectableClass type. */ providesClass = < - Class extends InjectableClass, - AdditionalDependencies extends ConstructorParameters, - Tokens extends Class["dependencies"], - Service extends ConstructorReturnType, Token extends TokenType, + Tokens extends readonly TokenType[], + Params extends readonly any[] & { length: Tokens["length"] }, + Service, >( token: Token, - cls: Class - ) => this.provides(ClassInjectable(token, cls)); + cls: { + readonly dependencies: Tokens; + new (...args: Params): Service; + } + ): PartialContainer< + AddService, + UpdateDependencies> + > => { + const injectable = (...args: Params) => new cls(...args); + injectable.dependencies = cls.dependencies; + injectable.token = token; + return this.provides(injectable); + }; /** * In order to create a [Container], the InjectableFunctions maintained by the PartialContainer must be memoized From 423ba6eb767025bd029560a334cd1e8c8f0d3a4f Mon Sep 17 00:00:00 2001 From: Mikalai Silivonik Date: Thu, 28 Nov 2024 11:18:11 -0500 Subject: [PATCH 08/14] Removed injectable class --- README.md | 1 - src/Injectable.ts | 53 +---------------------------------------- src/PartialContainer.ts | 2 +- src/index.ts | 2 +- src/types.ts | 12 ---------- 5 files changed, 3 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index c860256..42bea42 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,6 @@ db.save("user2"); // Log: Saving record: user2 - **Service**: Any value or instance provided by the Container. - **Token**: A unique identifier for each service, used for registration and retrieval within the Container. - **InjectableFunction**: Functions that return service instances. They can include dependencies which are injected when the service is requested. -- **InjectableClass**: Classes that can be instantiated by the Container. Dependencies should be specified in a static "dependencies" field to enable proper injection. ### API Reference diff --git a/src/Injectable.ts b/src/Injectable.ts index e718c16..20949d9 100644 --- a/src/Injectable.ts +++ b/src/Injectable.ts @@ -1,4 +1,4 @@ -import type { InjectableClass, InjectableFunction, ServicesFromTokenizedParams, TokenType } from "./types"; +import type { InjectableFunction, ServicesFromTokenizedParams, TokenType } from "./types"; /** * Creates an Injectable factory function designed for services without dependencies. @@ -117,57 +117,6 @@ export function InjectableCompat< return Injectable(token, dependencies, fn); } -/** - * Creates an Injectable factory function for an InjectableClass. - * - * @example - * ```ts - * class Logger { - * static dependencies = ["config"] as const; - * constructor(private config: string) {} - * public print() { - * console.log(this.config); - * } - * } - * - * const container = Container - * .providesValue("config", "value") - * .provides(ClassInjectable("logger", Logger)); - * - * container.get("logger").print(); // prints "value" - * ``` - * - * It is recommended to use the `Container.provideClass()` method. The example above is equivalent to: - * ```ts - * const container = Container - * .providesValue("config", "value") - * .providesClass("logger", Logger); - * container.get("logger").print(); // prints "value" - * ``` - * - * @param token Token identifying the Service. - * @param cls InjectableClass to instantiate. - */ -export function ClassInjectable< - Class extends InjectableClass, - Dependencies extends ConstructorParameters, - Token extends TokenType, - Tokens extends Class["dependencies"], ->( - token: Token, - cls: Class -): InjectableFunction, Tokens, Token, ConstructorReturnType>; - -export function ClassInjectable( - token: TokenType, - cls: InjectableClass -): InjectableFunction { - const factory = (...args: any[]) => new cls(...args); - factory.token = token; - factory.dependencies = cls.dependencies; - return factory; -} - /** * Creates an Injectable factory function without dependencies that appends a Service * to an existing array of Services of the same type. Useful for dynamically expanding diff --git a/src/PartialContainer.ts b/src/PartialContainer.ts index 955f2b1..98123e2 100644 --- a/src/PartialContainer.ts +++ b/src/PartialContainer.ts @@ -141,7 +141,7 @@ export class PartialContainer { * ``` * * @param token the Token by which the class will be known. - * @param cls the class to be provided, must match the InjectableClass type. + * @param cls the class to be provided. */ providesClass = < Token extends TokenType, diff --git a/src/index.ts b/src/index.ts index d17449d..25a5639 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ export { CONTAINER, Container } from "./Container"; export { Injectable, InjectableCompat, ConcatInjectable } from "./Injectable"; export { PartialContainer } from "./PartialContainer"; -export { InjectableFunction, InjectableClass, ServicesFromInjectables } from "./types"; +export { InjectableFunction, ServicesFromInjectables } from "./types"; diff --git a/src/types.ts b/src/types.ts index 294a97a..ccf55ce 100644 --- a/src/types.ts +++ b/src/types.ts @@ -53,18 +53,6 @@ export type MapTokensToTypes >; -/** - * Represents a class that can be used as an injectable service within a dependency injection {@link Container}. - * The `InjectableClass` type ensures that the class's dependencies and constructor signature align with - * the services available in the container, providing strong type safety. - */ -export type InjectableClass = Tokens extends readonly ValidTokens[] - ? { - readonly dependencies: Tokens; - new (...args: AsTuple>): Service; - } - : never; - export type AnyInjectable = InjectableFunction; /** From e2e337031b055f57de1d69dacf8f0ed1905f5cac Mon Sep 17 00:00:00 2001 From: Mikalai Silivonik Date: Thu, 28 Nov 2024 11:22:16 -0500 Subject: [PATCH 09/14] removed unused stuff --- src/Injectable.ts | 2 -- src/entries.ts | 4 ---- 2 files changed, 6 deletions(-) diff --git a/src/Injectable.ts b/src/Injectable.ts index 20949d9..1d75bde 100644 --- a/src/Injectable.ts +++ b/src/Injectable.ts @@ -205,5 +205,3 @@ export function ConcatInjectable( factory.dependencies = [token, ...dependencies]; return factory; } - -export type ConstructorReturnType = T extends new (...args: any) => infer C ? C : any; diff --git a/src/entries.ts b/src/entries.ts index cbc93ea..96114c1 100644 --- a/src/entries.ts +++ b/src/entries.ts @@ -1,7 +1,3 @@ // `Object.entries` does not use `keyof` types, so it loses type specificity. We'll fix this with a wrapper. export const entries = , U>(o: T): Array<[keyof T, T[keyof T]]> => Object.entries(o) as unknown as Array<[keyof T, T[keyof T]]>; - -// `Object.fromEntries` similarly does not preserve key types. -export const fromEntries = (entries: ReadonlyArray<[K, V]>): Record => - Object.fromEntries(entries) as Record; From eeaa7dd227cc9ba63f6d5dc9f9101515a7b7c37a Mon Sep 17 00:00:00 2001 From: Konstantin Burov Date: Fri, 29 Nov 2024 00:21:20 +1100 Subject: [PATCH 10/14] Fix constructor return type inference for published npm. # Conflicts: # src/Container.ts --- src/__tests__/Container.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/__tests__/Container.spec.ts b/src/__tests__/Container.spec.ts index 0a67495..705d8be 100644 --- a/src/__tests__/Container.spec.ts +++ b/src/__tests__/Container.spec.ts @@ -152,6 +152,7 @@ describe("Container", () => { } const containerWithService = container.providesClass("service", Item); expect(containerWithService.get("service")).toEqual(new Item(1)); + expect(containerWithService.factories.service().value).toBe(1); }); test("error if class constructor arity doesn't match dependencies", () => { From a305c529c7d3c7efa0946dabcad73ff62106756d Mon Sep 17 00:00:00 2001 From: Mikalai Silivonik Date: Thu, 28 Nov 2024 16:13:28 -0500 Subject: [PATCH 11/14] added comments --- src/PartialContainer.ts | 62 +++++++++++++++++++++++++++++++++++------ src/types.ts | 11 ++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/PartialContainer.ts b/src/PartialContainer.ts index 98123e2..b524bb1 100644 --- a/src/PartialContainer.ts +++ b/src/PartialContainer.ts @@ -5,19 +5,65 @@ import type { Container } from "./Container"; import type { AddService, InjectableFunction, ServicesFromTokenizedParams, TokenType, ValidTokens } from "./types"; import { Injectable } from "./Injectable"; -// Using a conditional type forces TS language services to evaluate the type -- so when showing e.g. type hints, we -// will see the mapped type instead of the AddDependencies type alias. This produces better hints. -type AddDependencies = ParentDependencies extends any +/** + * Combines two dependency maps into one, merging properties from both `ParentDependencies` and `Dependencies`. + * If a key exists in both, the value from `ParentDependencies` is used. + * + * This type is used to aggregate dependencies in a way that provides better type hints and readability in IDEs, + * because using a conditional type forces TypeScript to evaluate and display the resulting mapped type directly, + * rather than just the type alias name. + * + * @typeParam ExistingDependencies - The existing set of dependencies. + * @typeParam NewDependencies - The new dependencies to add. + * + * @remarks + * The use of a mapped type over an intersection type produces more concise and informative type hints. + * + * @example + * ```typescript + * type A = { foo: number; bar: string }; + * type B = { bar: boolean; baz: Date }; + * type Combined = AddDependencies; + * // Combined is { foo: number; bar: string; baz: Date } + * ``` + */ +type AddDependencies = ExistingDependencies extends any ? // A mapped type produces better, more concise type hints than an intersection type. { - [K in keyof ParentDependencies | keyof Dependencies]: K extends keyof ParentDependencies - ? ParentDependencies[K] - : K extends keyof Dependencies - ? Dependencies[K] + [K in keyof ExistingDependencies | keyof NewDependencies]: K extends keyof ExistingDependencies + ? ExistingDependencies[K] + : K extends keyof NewDependencies + ? NewDependencies[K] : never; } : never; +/** + * Updates the dependencies of a container by combining existing dependencies with new ones, + * while excluding any dependencies that are already satisfied by the container's existing services + * or the new service being provided. + * + * Specifically: + * - It removes the `NewToken` from `ExistingDependencies` to avoid circular dependencies. + * - It excludes keys from `NewDependencies` that are already present in `ExistingServices` + * since those dependencies are already resolved. + * - It then combines the resulting dependencies into a new dependency map. + * + * @typeParam ExistingServices - The services already provided by the container. + * @typeParam ExistingDependencies - The current dependencies of the container. + * @typeParam NewToken - The token of the new service being added to the container. + * @typeParam NewDependencies - The dependencies required by the new service. + * + * @example + * ```typescript + * type ExistingServices = { foo: number }; + * type ExistingDependencies = { bar: string; baz: boolean }; + * type NewToken = 'qux'; + * type NewDependencies = { baz: boolean; quux: Date }; + * type Updated = UpdateDependencies; + * // Updated is { bar: string; quux: Date } + * ``` + */ type UpdateDependencies< ExistingServices, ExistingDependencies, @@ -161,7 +207,7 @@ export class PartialContainer { const injectable = (...args: Params) => new cls(...args); injectable.dependencies = cls.dependencies; injectable.token = token; - return this.provides(injectable); + return this.provides(injectable); }; /** diff --git a/src/types.ts b/src/types.ts index ccf55ce..eaa2939 100644 --- a/src/types.ts +++ b/src/types.ts @@ -49,6 +49,17 @@ export type InjectableFunction< } : never; +/** + * Maps a tuple of tokens to their corresponding service types based on the given Services type mapping. + * + * @example + * ```typescript + * type Services = { foo: string; bar: number; }; + * type Tokens = ['foo', 'bar']; + * type Result = MapTokensToTypes; + * // Result is [string, number] + * ``` + */ export type MapTokensToTypes[]> = AsTuple< CorrespondingServices >; From ea9cd7e5168f6f265a8b65f5d298a0f7c3a713a8 Mon Sep 17 00:00:00 2001 From: Mikalai Silivonik Date: Fri, 29 Nov 2024 12:17:17 -0500 Subject: [PATCH 12/14] cleanup --- src/Injectable.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Injectable.ts b/src/Injectable.ts index 1d75bde..1bc4a36 100644 --- a/src/Injectable.ts +++ b/src/Injectable.ts @@ -80,7 +80,6 @@ export function Injectable( ); } - // const length = actualLength in fn ? fn[actualLength] as number : fn.length; if (fn.length !== 0 && fn.length !== dependencies.length) { throw new TypeError( "[Injectable] Function arity does not match the number of dependencies. Function has arity " + From e2b8cf8d219761f8369a34a161c6e859e0624f0d Mon Sep 17 00:00:00 2001 From: Mikalai Silivonik Date: Mon, 2 Dec 2024 14:21:10 -0500 Subject: [PATCH 13/14] 0.4.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 984f050..b62decd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@snap/ts-inject", - "version": "0.3.1", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@snap/ts-inject", - "version": "0.3.1", + "version": "0.4.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.12", diff --git a/package.json b/package.json index 969e9f8..0c95e3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@snap/ts-inject", - "version": "0.3.2", + "version": "0.4.0", "description": "100% typesafe dependency injection framework for TypeScript projects", "license": "MIT", "author": "Snap Inc.", From 73ce0303208e4eea35cd683782da3a1897c65286 Mon Sep 17 00:00:00 2001 From: Mikalai Silivonik Date: Mon, 2 Dec 2024 14:23:55 -0500 Subject: [PATCH 14/14] fixed lint issue --- src/Container.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Container.ts b/src/Container.ts index 9e58fc9..aeebbaa 100644 --- a/src/Container.ts +++ b/src/Container.ts @@ -421,7 +421,7 @@ export class Container { Token extends TokenType, Tokens extends readonly ValidTokens[], Params extends MapTokensToTypes, - Service + Service, >( token: Token, cls: { @@ -487,7 +487,7 @@ export class Container { Token extends keyof Services, Tokens extends readonly ValidTokens[], Params extends MapTokensToTypes, - Service + Service, >( token: Token, cls: {