-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathInjectable.ts
261 lines (245 loc) · 9.89 KB
/
Injectable.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
import type { InjectableClass, InjectableFunction, ServicesFromTokenizedParams, TokenType } from "./types";
/**
* Creates an Injectable factory function designed for services without dependencies.
* This is useful for simple services or values that don't depend on other parts of the system.
*
* @example
* ```ts
* const container = Container.provides(Injectable("MyService", () => new MyService()));
*
* const myService = container.get("MyService");
* ```
*
* @param token A unique Token identifying the Service within the container. This token
* is used to retrieve the instance from the container.
* @param fn A zero-argument function that initializes and returns the Service instance.
* This can be any class instance, primitive, or complex value meant to be managed
* within the DI container.
*/
export function Injectable<Token extends TokenType, Service>(
token: Token,
fn: () => Service
): InjectableFunction<any, [], Token, Service>;
/**
* Creates an Injectable factory function that requires dependencies.
*
* The dependencies are specified as tokens, and the factory function
* will receive these dependencies as arguments in the order they are listed.
*
* **Important:** This function requires **TypeScript 5 or later** due to the use of `const` type parameters.
* Users on TypeScript 4 and earlier must use {@link InjectableCompat} instead.
*
* @example
* ```ts
* const dependencyB = 'DependencyB';
* const container = Container
* .providesValue("DependencyA", new A())
* .providesValue("DependencyB", new B())
* .provides(Injectable(
* "MyService",
* ["DependencyA", dependencyB] as const, // "as const" can be omitted in TypeScript 5 and later
* (a: A, b: B) => new MyService(a, b),
* )
* )
*
* const myService = container.get("MyService");
* ```
*
* @param token A unique Token identifying the Service within the container.
* @param dependencies A *readonly* array of Tokens representing the dependencies required by the factory function.
* These will be resolved by the container and provided as arguments to the factory function.
* @param fn A factory function whose parameters match the dependencies. This function should initialize and
* return an instance of the Service. The types and number of its parameters must exactly match the dependencies.
*/
export function Injectable<
Token extends TokenType,
const Tokens extends readonly TokenType[],
Params extends readonly any[],
Service,
>(
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<ServicesFromTokenizedParams<Tokens, Params>, Tokens, Token, Service>
: never;
export function Injectable(
token: TokenType,
dependenciesOrFn?: readonly TokenType[] | (() => any),
maybeFn?: (...args: any[]) => any
): InjectableFunction<any, readonly TokenType[], TokenType, any> {
const dependencies: TokenType[] = Array.isArray(dependenciesOrFn) ? dependenciesOrFn : [];
const fn = typeof dependenciesOrFn === "function" ? dependenciesOrFn : maybeFn;
if (!fn) {
throw new TypeError(
"[Injectable] Received invalid arguments. The factory function must be either the second " + "or third argument."
);
}
if (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.` +
`\nDependencies: ${JSON.stringify(dependencies)}`
);
}
const factory = (...args: any[]) => fn(...args);
factory.token = token;
factory.dependencies = dependencies;
return factory;
}
/**
* A compatibility version of {@link Injectable} for TypeScript 4 and earlier users.
* This function behaves identically to {@link Injectable} but requires the use of `as const` on the dependencies array.
*
* @deprecated Use {@link Injectable} instead. This function is provided for compatibility with TypeScript 4
* and earlier versions and will be removed in future releases.
*
* @see {@link Injectable} for detailed usage instructions and examples.
*/
export function InjectableCompat<
Token extends TokenType,
Tokens extends readonly TokenType[],
Params extends readonly any[],
Service,
>(
token: Token,
dependencies: Tokens,
fn: (...args: Tokens["length"] extends Params["length"] ? Params : void[]) => Service
): ReturnType<typeof Injectable> {
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<any, any, any>,
Dependencies extends ConstructorParameters<Class>,
Token extends TokenType,
Tokens extends Class["dependencies"],
>(
token: Token,
cls: Class
): InjectableFunction<ServicesFromTokenizedParams<Tokens, Dependencies>, Tokens, Token, ConstructorReturnType<Class>>;
export function ClassInjectable(
token: TokenType,
cls: InjectableClass<any, any, readonly TokenType[]>
): InjectableFunction<any, readonly TokenType[], TokenType, any> {
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
* service collections without altering original service tokens or factories.
*
* @example
* ```ts
* const container = Container
* .providesValue("values", [1]) // Initially provide an array with one value
* .provides(ConcatInjectable("values", () => 2)); // Append another value to the array
*
* const result = container.get("values"); // Results in [1, 2]
* ```
*
* In this context, `ConcatInjectable("values", () => 2)` acts as a simplified form of
* `Injectable("values", ["values"], (values: number[]) => [...values, 2])`,
* directly appending a new value to the "values" service array without the need for explicit array manipulation.
*
* @param token Token identifying an existing Service array to which the new Service will be appended.
* @param fn A no-argument function that returns the service to be appended.
*/
export function ConcatInjectable<Token extends TokenType, Service>(
token: Token,
fn: () => Service
): InjectableFunction<{ [T in keyof Token]: Service[] }, [], Token, Service[]>;
/**
* Creates an Injectable factory function with dependencies that appends a Service
* to an existing array of Services of the same type. This variant supports services
* that require other services to be instantiated, allowing for more complex setups.
*
* @example
* ```ts
* const container = Container
* .providesValue("two", 2)
* .providesValue("values", [1]) // Initially provide an array with one value
* .provides(ConcatInjectable("values", ["two"] as const, (two: number) => two)); // Append another value to the array
*
* const result = container.get("values"); // [1, 2]
* ```
*
* @param token Token identifying an existing Service array to append the new Service to.
* @param dependencies Read-only list of Tokens for dependencies required by the factory function.
* @param fn Factory function returning the Service to append.
* The types and number of its parameters must exactly match the dependencies.
*/
export function ConcatInjectable<
Token extends TokenType,
const Tokens extends readonly TokenType[],
Params extends readonly any[],
Service,
>(
token: Token,
dependencies: Tokens,
fn: (...args: Tokens["length"] extends Params["length"] ? Params : void[]) => Service
): InjectableFunction<ServicesFromTokenizedParams<Tokens, Params>, Tokens, Token, Service[]>;
export function ConcatInjectable(
token: TokenType,
dependenciesOrFn?: readonly TokenType[] | (() => any),
maybeFn?: (...args: any[]) => any
): InjectableFunction<any, readonly TokenType[], TokenType, any[]> {
const dependencies: TokenType[] = Array.isArray(dependenciesOrFn) ? dependenciesOrFn : [];
const fn = typeof dependenciesOrFn === "function" ? dependenciesOrFn : maybeFn;
if (!fn) {
throw new TypeError(
"[ConcatInjectable] Received invalid arguments. The factory function must be either the second " +
"or third argument."
);
}
if (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.` +
`\nDependencies: ${JSON.stringify(dependencies)}`
);
}
const factory = (array: any[], ...args: any[]) => {
return array.concat(fn(...args));
};
factory.token = token;
factory.dependencies = [token, ...dependencies];
return factory;
}
export type ConstructorReturnType<T> = T extends new (...args: any) => infer C ? C : any;