Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make ClassInjectable infer dependency types. Update docs with an example that compiles. #9

Merged
merged 2 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/Container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,9 +512,12 @@ export class Container<Services = {}> {
ConcatInjectable(fn.token, () => this.providesService(fn).get(fn.token))
) as Container<Services>;

private providesService<Token extends TokenType, Tokens extends readonly ValidTokens<Services>[], Service>(
fn: InjectableFunction<Services, Tokens, Token, Service>
): Container<AddService<Services, Token, Service>> {
private providesService<
Token extends TokenType,
Tokens extends readonly ValidTokens<Services>[],
Service,
Dependencies,
>(fn: InjectableFunction<Dependencies, Tokens, Token, Service>): Container<AddService<Services, Token, Service>> {
const token = fn.token;
const dependencies: readonly any[] = fn.dependencies;
// If the service depends on itself, e.g. in the multi-binding case, where we call append multiple times with
Expand Down
44 changes: 27 additions & 17 deletions src/Injectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,33 +105,41 @@ export function Injectable(
*
* @example
* ```ts
* class InjectableClassService {
* static dependencies = ["service"] as const;
* constructor(public service: string) {}
* public print(): string {
* console.log(this.service);
* }
* class Logger {
* static dependencies = ["config"] as const;
* constructor(private config: string) {}
* public print() {
* console.log(this.config);
* }
* }
*
* let container = Container.provides("service", "service value")
* .provides(ClassInjectable("classService", InjectableClassService));
*
* container.get("classService").print(); // prints "service value"
* const container = Container
* .providesValue("config", "value")
* .provides(ClassInjectable("logger", Logger));
*
* // prefer using Container's provideClass method. Above is the equivalent of:
* container = Container.provides("service", "service value")
* .providesClass("classService", InjectableClassService);
* container.get("logger").print(); // prints "value"
* ```
*
* container.get("classService").print(); // prints "service 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<Services, Token extends TokenType, const Tokens extends readonly TokenType[], Service>(
export function ClassInjectable<
Class extends InjectableClass<any, any, any>,
Dependencies extends ConstructorParameters<Class>,
Token extends TokenType,
Tokens extends Class["dependencies"],
>(
token: Token,
cls: InjectableClass<Services, Service, Tokens>
): InjectableFunction<Services, Tokens, Token, Service>;
cls: Class
): InjectableFunction<ServicesFromTokenizedParams<Tokens, Dependencies>, Tokens, Token, ConstructorReturnType<Class>>;

export function ClassInjectable(
token: TokenType,
Expand Down Expand Up @@ -230,3 +238,5 @@ export function ConcatInjectable(
factory.dependencies = [token, ...dependencies];
return factory;
}

export type ConstructorReturnType<T> = T extends new (...args: any) => infer C ? C : any;
5 changes: 2 additions & 3 deletions src/PartialContainer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { entries } from "./entries";
import { memoize } from "./memoize";
import type { Memoized } from "./memoize";
import { memoize } from "./memoize";
import type { Container } from "./Container";
import type {
AddService,
Expand All @@ -10,10 +10,9 @@ import type {
TokenType,
ValidTokens,
} from "./types";
import type { ConstructorReturnType } from "./Injectable";
import { ClassInjectable, Injectable } from "./Injectable";

type ConstructorReturnType<T> = T extends new (...args: any) => infer C ? C : any;

// 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, Dependencies> = ParentDependencies extends any
Expand Down