diff --git a/package.json b/package.json index e56c928..fb8f52f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@snap/ts-inject", - "version": "0.1.0", + "version": "0.1.1", "description": "100% typesafe dependency injection framework for TypeScript projects", "license": "MIT", "author": "Snap Inc.", diff --git a/src/Container.ts b/src/Container.ts index 0694c34..a75811b 100644 --- a/src/Container.ts +++ b/src/Container.ts @@ -1,7 +1,7 @@ import { isMemoized, memoize } from "./memoize"; import type { Memoized } from "./memoize"; import { PartialContainer } from "./PartialContainer"; -import type { AddService, InjectableClass, InjectableFunction, TokenType, ValidTokens } from "./types"; +import type { AddService, AddServices, InjectableClass, InjectableFunction, TokenType, ValidTokens } from "./types"; import { ConcatInjectable } from "./Injectable"; import { entries } from "./entries"; @@ -344,7 +344,7 @@ export class Container { // `this` type, we ensure this Container can provide all the Dependencies required by the PartialContainer. this: Container, container: PartialContainer - ): Container; + ): Container>; /** * Merges services from another `Container` into this container, creating a new `Container` instance. @@ -362,7 +362,9 @@ export class Container { * @returns A new `Container` instance that combines services from this container with those from the * provided container, with services from the provided container taking precedence in case of conflicts. */ - provides(container: Container): Container; + provides( + container: Container + ): Container>; /** * Registers a new service in this Container using an `InjectableFunction`. This function defines how the service @@ -400,7 +402,7 @@ export class Container { return new Container({ ...this.factories, ...factories, - } as unknown as MaybeMemoizedFactories); + } as unknown as MaybeMemoizedFactories>); } return this.providesService(fnOrContainer); } diff --git a/src/__tests__/Container.spec.ts b/src/__tests__/Container.spec.ts index 825fbca..10523f1 100644 --- a/src/__tests__/Container.spec.ts +++ b/src/__tests__/Container.spec.ts @@ -305,6 +305,27 @@ describe("Container", () => { let childContainerWithOverride = parentContainer.providesValue("value", 2); expect(childContainerWithOverride.get("service")).toBe(1); }); + + test("overriding with a different type changes resulting container's type", () => { + const parentContainer = Container.providesValue("value", 1); + let childContainerWithOverride = parentContainer.providesValue("value", "two"); + + // @ts-expect-error should be failing to compile as the type of the container has changed + let numberValue: number = childContainerWithOverride.get("value"); + + let value: string = childContainerWithOverride.get("value"); + expect(value).toBe("two"); + + const partialContainer = new PartialContainer({}).provides(Injectable("value", () => "three")); + childContainerWithOverride = parentContainer.provides(partialContainer); + value = childContainerWithOverride.get("value"); + expect(value).toBe("three"); + + let extraContainer = Container.fromObject({ value: "four" }); + childContainerWithOverride = parentContainer.provides(extraContainer); + value = childContainerWithOverride.get("value"); + expect(value).toBe("four"); + }); }); describe("when making a copy of the Container", () => { diff --git a/src/types.ts b/src/types.ts index e590849..b9dc936 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,7 +71,30 @@ export type ServicesFromInjectables = ParentServices extends any ? // A mapped type produces better, more concise type hints than an intersection type. - { [K in keyof ParentServices | Token]: K extends keyof ParentServices ? ParentServices[K] : Service } + { + [K in keyof ParentServices | Token]: K extends keyof ParentServices + ? K extends Token + ? Service + : ParentServices[K] + : Service; + } + : never; + +/** + * Same as AddService above, but is merging multiple services at once. Services types override those of the parent. + */ +// 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 AddService type alias. This produces better hints. +export type AddServices = ParentServices extends any + ? Services extends any + ? { + [K in keyof Services | keyof ParentServices]: K extends keyof Services + ? Services[K] + : K extends keyof ParentServices + ? ParentServices[K] + : never; + } + : never : never; /**