Skip to content

Commit d85d8f1

Browse files
authored
Correct types for overriden services (#4)
Fixing an issue where override's type wasn't correctly carried over onto the resulting container, e.g. ``` const value: number = Container.providesValue("value", 1).providesValue("value", "two").get("value"); ``` would, incorrectly, compile and cause issues at runtime. With the fix `.get("value")` return type is correctly resolved to `string`. Also fixed `provides*` implementations that take containers as arguments.
1 parent 818bb29 commit d85d8f1

File tree

4 files changed

+52
-6
lines changed

4 files changed

+52
-6
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@snap/ts-inject",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"description": "100% typesafe dependency injection framework for TypeScript projects",
55
"license": "MIT",
66
"author": "Snap Inc.",

src/Container.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { isMemoized, memoize } from "./memoize";
22
import type { Memoized } from "./memoize";
33
import { PartialContainer } from "./PartialContainer";
4-
import type { AddService, InjectableClass, InjectableFunction, TokenType, ValidTokens } from "./types";
4+
import type { AddService, AddServices, InjectableClass, InjectableFunction, TokenType, ValidTokens } from "./types";
55
import { ConcatInjectable } from "./Injectable";
66
import { entries } from "./entries";
77

@@ -344,7 +344,7 @@ export class Container<Services = {}> {
344344
// `this` type, we ensure this Container can provide all the Dependencies required by the PartialContainer.
345345
this: Container<FulfilledDependencies>,
346346
container: PartialContainer<AdditionalServices, Dependencies>
347-
): Container<Services & AdditionalServices>;
347+
): Container<AddServices<Services, AdditionalServices>>;
348348

349349
/**
350350
* Merges services from another `Container` into this container, creating a new `Container` instance.
@@ -362,7 +362,9 @@ export class Container<Services = {}> {
362362
* @returns A new `Container` instance that combines services from this container with those from the
363363
* provided container, with services from the provided container taking precedence in case of conflicts.
364364
*/
365-
provides<AdditionalServices>(container: Container<AdditionalServices>): Container<Services & AdditionalServices>;
365+
provides<AdditionalServices>(
366+
container: Container<AdditionalServices>
367+
): Container<AddServices<Services, AdditionalServices>>;
366368

367369
/**
368370
* Registers a new service in this Container using an `InjectableFunction`. This function defines how the service
@@ -400,7 +402,7 @@ export class Container<Services = {}> {
400402
return new Container({
401403
...this.factories,
402404
...factories,
403-
} as unknown as MaybeMemoizedFactories<Services & AdditionalServices>);
405+
} as unknown as MaybeMemoizedFactories<AddServices<Services, AdditionalServices>>);
404406
}
405407
return this.providesService(fnOrContainer);
406408
}

src/__tests__/Container.spec.ts

+21
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,27 @@ describe("Container", () => {
305305
let childContainerWithOverride = parentContainer.providesValue("value", 2);
306306
expect(childContainerWithOverride.get("service")).toBe(1);
307307
});
308+
309+
test("overriding with a different type changes resulting container's type", () => {
310+
const parentContainer = Container.providesValue("value", 1);
311+
let childContainerWithOverride = parentContainer.providesValue("value", "two");
312+
313+
// @ts-expect-error should be failing to compile as the type of the container has changed
314+
let numberValue: number = childContainerWithOverride.get("value");
315+
316+
let value: string = childContainerWithOverride.get("value");
317+
expect(value).toBe("two");
318+
319+
const partialContainer = new PartialContainer({}).provides(Injectable("value", () => "three"));
320+
childContainerWithOverride = parentContainer.provides(partialContainer);
321+
value = childContainerWithOverride.get("value");
322+
expect(value).toBe("three");
323+
324+
let extraContainer = Container.fromObject({ value: "four" });
325+
childContainerWithOverride = parentContainer.provides(extraContainer);
326+
value = childContainerWithOverride.get("value");
327+
expect(value).toBe("four");
328+
});
308329
});
309330

310331
describe("when making a copy of the Container", () => {

src/types.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,30 @@ export type ServicesFromInjectables<Injectables extends readonly AnyInjectable[]
7171
// will see the mapped type instead of the AddService type alias. This produces better hints.
7272
export type AddService<ParentServices, Token extends TokenType, Service> = ParentServices extends any
7373
? // A mapped type produces better, more concise type hints than an intersection type.
74-
{ [K in keyof ParentServices | Token]: K extends keyof ParentServices ? ParentServices[K] : Service }
74+
{
75+
[K in keyof ParentServices | Token]: K extends keyof ParentServices
76+
? K extends Token
77+
? Service
78+
: ParentServices[K]
79+
: Service;
80+
}
81+
: never;
82+
83+
/**
84+
* Same as AddService above, but is merging multiple services at once. Services types override those of the parent.
85+
*/
86+
// Using a conditional type forces TS language services to evaluate the type -- so when showing e.g. type hints, we
87+
// will see the mapped type instead of the AddService type alias. This produces better hints.
88+
export type AddServices<ParentServices, Services> = ParentServices extends any
89+
? Services extends any
90+
? {
91+
[K in keyof Services | keyof ParentServices]: K extends keyof Services
92+
? Services[K]
93+
: K extends keyof ParentServices
94+
? ParentServices[K]
95+
: never;
96+
}
97+
: never
7598
: never;
7699

77100
/**

0 commit comments

Comments
 (0)