Skip to content

Commit 1564a20

Browse files
committed
🏷️ fix(strongly-typed) allow use of generic helpers
Fixes inversify#170 At the moment it's impossible to create generic helper functions to deal with `TypedContainer`: ```ts interface MyMap { foo: string; } function fn<T extends MyMap>(container: TypedContainer<T>) { container.get('foo') // error } ``` This is because of the `Synchronous` type that guards against calling `.get()` on `Promise` bindings. Under the hood, this type is a mapped type, which [doesn't work well with generics][1] (by design). Rather than drop this guard all together, this change aims to strike a balance by removing the `Synchronous` mapped type, and instead changing the return type of synchronous `get()` methods to be `never` if the binding is a `Promise`. This won't error as obviously or as immediately as before, but will still at least flag to the developer semantically that this binding will never return a value (since it will throw), and should cause compilation errors if consumers try to do anything with the returned value. In return, we gain the ability to use generic helper functions. [1]: microsoft/TypeScript#35647
1 parent 04094c0 commit 1564a20

File tree

3 files changed

+55
-28
lines changed

3 files changed

+55
-28
lines changed

.changeset/hot-lobsters-allow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@inversifyjs/strongly-typed": patch
3+
---
4+
5+
Fixed types to allow use of generic helper functions

packages/container/libraries/strongly-typed/src/container.spec.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,13 @@ describe('interfaces', () => {
120120
});
121121

122122
it('gets a promise', async () => {
123-
// @ts-expect-error :: can't call get() to get Promise
124-
expect(() => container.get('asyncNumber')).toThrow(
125-
'it has asynchronous dependencies',
126-
);
123+
expect(() => {
124+
const num = container.get('asyncNumber');
125+
/* eslint-disable @typescript-eslint/no-unused-expressions */
126+
// @ts-expect-error :: num is never
127+
num.then;
128+
/* eslint-enable @typescript-eslint/no-unused-expressions */
129+
}).toThrow('it has asynchronous dependencies');
127130
const n: Promise<number> = container.getAsync('asyncNumber');
128131
expect(await n).toBe(1);
129132
});
@@ -201,6 +204,29 @@ describe('interfaces', () => {
201204
container.bind('foo').to(Bar);
202205
});
203206
});
207+
208+
describe('generics', () => {
209+
beforeEach(() => {
210+
container.bind('foo').to(Foo);
211+
});
212+
213+
it('can be used in a generic function', () => {
214+
function test<T extends BindingMap>(
215+
container: TypedContainer<T>,
216+
): void {
217+
const foo: Foo = container.get('foo');
218+
// @ts-expect-error :: can't assign Foo to Bar
219+
const bar: Bar = container.get('foo');
220+
221+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
222+
foo;
223+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
224+
bar;
225+
}
226+
227+
test(container);
228+
});
229+
});
204230
});
205231
});
206232
});

packages/container/libraries/strongly-typed/src/container.ts

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
/* eslint-disable @typescript-eslint/no-unnecessary-type-parameters */
2+
33
import { Container, type interfaces } from 'inversify';
44

55
type IfAny<T, TYes, TNo> = 0 extends 1 & T ? TYes : TNo;
@@ -23,11 +23,7 @@ type ContainerBinding<
2323
? any
2424
: never;
2525

26-
type Synchronous<T extends BindingMap> = IfAny<
27-
T,
28-
any,
29-
{ [K in keyof T as T[K] extends Promise<any> ? never : K]: T[K] }
30-
>;
26+
type NeverPromise<T> = T extends Promise<any> ? never : T;
3127

3228
type First<T extends any[]> = T extends [infer TFirst, ...any[]]
3329
? TFirst
@@ -44,47 +40,47 @@ interface ContainerOverrides<
4440
parent: ContainerOverrides<First<TAncestors>, AllButFirst<TAncestors>> | null;
4541
bind: Bind<T>;
4642
get: <
47-
TBound extends ContainerBinding<Synchronous<T>, TKey>,
48-
TKey extends MappedServiceIdentifier<Synchronous<T>> = any,
43+
TBound extends ContainerBinding<T, TKey>,
44+
TKey extends MappedServiceIdentifier<T> = any,
4945
>(
5046
serviceIdentifier: TKey,
51-
) => TBound;
47+
) => NeverPromise<TBound>;
5248
getNamed: <
53-
TBound extends ContainerBinding<Synchronous<T>, TKey>,
54-
TKey extends MappedServiceIdentifier<Synchronous<T>> = any,
49+
TBound extends ContainerBinding<T, TKey>,
50+
TKey extends MappedServiceIdentifier<T> = any,
5551
>(
5652
serviceIdentifier: TKey,
5753
named: PropertyKey,
58-
) => TBound;
54+
) => NeverPromise<TBound>;
5955
getTagged: <
60-
TBound extends ContainerBinding<Synchronous<T>, TKey>,
61-
TKey extends MappedServiceIdentifier<Synchronous<T>> = any,
56+
TBound extends ContainerBinding<T, TKey>,
57+
TKey extends MappedServiceIdentifier<T> = any,
6258
>(
6359
serviceIdentifier: TKey,
6460
key: PropertyKey,
6561
value: unknown,
66-
) => TBound;
62+
) => NeverPromise<TBound>;
6763
getAll: <
68-
TBound extends ContainerBinding<Synchronous<T>, TKey>,
69-
TKey extends MappedServiceIdentifier<Synchronous<T>> = any,
64+
TBound extends ContainerBinding<T, TKey>,
65+
TKey extends MappedServiceIdentifier<T> = any,
7066
>(
7167
serviceIdentifier: TKey,
72-
) => TBound[];
68+
) => NeverPromise<TBound[]>;
7369
getAllTagged: <
74-
TBound extends ContainerBinding<Synchronous<T>, TKey>,
75-
TKey extends MappedServiceIdentifier<Synchronous<T>> = any,
70+
TBound extends ContainerBinding<T, TKey>,
71+
TKey extends MappedServiceIdentifier<T> = any,
7672
>(
7773
serviceIdentifier: TKey,
7874
key: PropertyKey,
7975
value: unknown,
80-
) => TBound[];
76+
) => NeverPromise<TBound[]>;
8177
getAllNamed: <
82-
TBound extends ContainerBinding<Synchronous<T>, TKey>,
83-
TKey extends MappedServiceIdentifier<Synchronous<T>> = any,
78+
TBound extends ContainerBinding<T, TKey>,
79+
TKey extends MappedServiceIdentifier<T> = any,
8480
>(
8581
serviceIdentifier: TKey,
8682
named: PropertyKey,
87-
) => TBound[];
83+
) => NeverPromise<TBound[]>;
8884
getAsync: <
8985
TBound extends ContainerBinding<T, TKey>,
9086
TKey extends MappedServiceIdentifier<T> = any,

0 commit comments

Comments
 (0)