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

Improved docs #13

Merged
merged 2 commits into from
Nov 20, 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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@snap/ts-inject",
"version": "0.3.0",
"version": "0.3.1",
"description": "100% typesafe dependency injection framework for TypeScript projects",
"license": "MIT",
"author": "Snap Inc.",
Expand Down
99 changes: 99 additions & 0 deletions src/__tests__/types.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Injectable } from "../Injectable";
import type { ServicesFromInjectables } from "../types";
import { Container } from "../Container";

describe("ServicesFromInjectables", () => {
test("correctly maps injectables to service types and allow container type definition before construction.", () => {
const injectable1 = Injectable("Service1", () => "service1");
const injectable2 = Injectable("Service2", () => 42);

const injectables = [injectable1, injectable2] as const;

// Use ServicesFromInjectables to derive the services' types
type Services = ServicesFromInjectables<typeof injectables>;

// Services type is equivalent to:
// {
// Service1: string;
// Service2: number;
// }

// Declare a container variable with the derived Services type
// This allows us to reference the container with accurate typing before it's constructed,
// ensuring type safety and enabling its use in type annotations elsewhere
let container: Container<Services>;

// Assign the container with the actual instance
container = Container.provides(injectable1).provides(injectable2);

// Retrieve services with accurate typing
const service1 = container.get("Service1"); // Type: string
const service2 = container.get("Service2"); // Type: number

// @ts-expect-error
expect(() => container.get("NonExistentService")).toThrow();

// @ts-expect-error
const invalidService1: number = container.get("Service1");
// @ts-expect-error
const invalidService2: string = container.get("Service2");

// Use the services
expect(service1).toBe("service1");
expect(service2).toBe(42);
});

test("handles injectables with dependencies and allow pre-definition of container type", () => {
const injectableDep = Injectable("DepService", () => 100);
const injectableMain = Injectable("MainService", ["DepService"] as const, (dep: number) => dep + 1);

const injectables = [injectableDep, injectableMain] as const;

type Services = ServicesFromInjectables<typeof injectables>;

let container: Container<Services>;

container = Container.provides(injectableDep).provides(injectableMain);

expect(container.get("DepService")).toBe(100);
expect(container.get("MainService")).toBe(101);
});

test("enforces type safety when assigning services.", () => {
const injectable1 = Injectable("Service1", () => "service1");
const injectable2 = Injectable("Service2", () => 42);

const injectables = [injectable1, injectable2] as const;

type Services = ServicesFromInjectables<typeof injectables>;

// Correct assignment
const services: Services = {
Service1: "service1",
Service2: 42,
};

// Attempting incorrect assignments should result in TypeScript errors

const invalidServices1: Services = {
Service1: "service1",
// @ts-expect-error
Service2: "not a number", // Error: Type 'string' is not assignable to type 'number'
};

const invalidServices2: Services = {
// @ts-expect-error
Service1: 123, // Error: Type 'number' is not assignable to type 'string'
Service2: 42,
};

// @ts-expect-error
const invalidServices3: Services = {
Service1: "service1",
// Missing 'Service2' property
};

// avoid the "unused variable" TypeScript error
expect(services ?? invalidServices1 ?? invalidServices2 ?? invalidServices3).toBeDefined();
});
});
49 changes: 46 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ export type CorrespondingServices<Services, Tokens extends readonly ValidTokens<
};

/**
* A valid InjectableFunction is one that can be successfully called, given some Services, to return a new Service. That
* is, it must satisfy two conditions:
* A valid `InjectableFunction` is one that can be successfully called, given some Services, to return a new Service.
* That is, it must satisfy two conditions:
*
* 1. All the Tokens it specifies as dependencies are valid given the Services (i.e. they are either the Container
* Token or keys of the Services type).
* 2. The function argument types correspond to the Services specified by the dependency Tokens.
*
* A InjectableFunction also includes its own key Token and dependency Tokens as metadata, so it may be resolved by
* A `InjectableFunction` also includes its own key Token and dependency Tokens as metadata, so it may be resolved by
* Container<Services> later.
*/
export type InjectableFunction<
Expand All @@ -49,6 +49,11 @@ export type InjectableFunction<
}
: never;

/**
* Represents a class that can be used as an injectable service within a dependency injection {@link Container}.
* The `InjectableClass` type ensures that the class's dependencies and constructor signature align with
* the services available in the container, providing strong type safety.
*/
export type InjectableClass<Services, Service, Tokens> = Tokens extends readonly ValidTokens<Services>[]
? {
readonly dependencies: Tokens;
Expand All @@ -58,6 +63,44 @@ export type InjectableClass<Services, Service, Tokens> = Tokens extends readonly

export type AnyInjectable = InjectableFunction<any, readonly TokenType[], TokenType, any>;

/**
* Maps an array of {@link InjectableFunction} to a service type object, where each key is the token of an
* {@link Injectable}, and the corresponding value is the return type of that {@link Injectable}.
*
* This utility type is useful for deriving the service types provided by a collection of {@link InjectableFunction}s,
* ensuring type safety and consistency throughout your application.
*
* You can use `ServicesFromInjectables` to construct a type that serves as a type parameter for a {@link Container},
* allowing the container's type to accurately reflect the services it provides,
* even before the container is constructed.
*
* @typeParam Injectables - A tuple of {@link InjectableFunction}s.
*
* @example
* // Define some Injectable functions
* const injectable1 = Injectable("Service1", () => "service1");
* const injectable2 = Injectable("Service2", () => 42);
*
* // Collect them in a tuple
* const injectables = [injectable1, injectable2] as const;
*
* // Use ServicesFromInjectables to derive the services' types
* type Services = ServicesFromInjectables<typeof injectables>;
*
* // Services type is equivalent to:
* // {
* // Service1: string;
* // Service2: number;
* // }
*
* // Declare a container variable with the derived Services type
* // This allows us to reference the container with accurate typing before it's constructed,
* // ensuring type safety and enabling its use in type annotations elsewhere
* let container: Container<Services>;
*
* // Assign the container with the actual instance
* container = Container.provides(injectable1).provides(injectable2);
*/
export type ServicesFromInjectables<Injectables extends readonly AnyInjectable[]> = {
[Name in Injectables[number]["token"]]: ReturnType<Extract<Injectables[number], { token: Name }>>;
};
Expand Down