Skip to content

Commit

Permalink
Make ClassInjectable infer dependency types. Update docs with an exam…
Browse files Browse the repository at this point in the history
…ple that compiles. (#9)

Addresses #8, examples from
the docs now compile, also makes ClassInjectable usable directly as it
now infers class's dependencies.

---------

Co-authored-by: Mikalai Silivonik <[email protected]>
  • Loading branch information
kburov-sc and mikalai-snap authored Oct 22, 2024
1 parent d716d2a commit 60a89bf
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 23 deletions.
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

0 comments on commit 60a89bf

Please sign in to comment.