Skip to content

Tokens and providers

A token is a unique, type-carrying key. Identity is based on an internal symbol, not its diagnostic name, so two tokens named CONFIG are still different.

import { createToken } from "di-craft";
type Config = { readonly apiUrl: string };
const CONFIG = createToken<Config>("CONFIG");
// Token<Config>

The value type flows through providers and resolution:

const config = container.get(CONFIG); // Config

Use provideValue when the dependency already exists or was initialized before the container:

const providers = [
provideValue(CONFIG, { apiUrl: "https://api.example.com" }),
];

Values behave like singleton dependencies and do not have a disposal hook.

Factories create a value lazily. The keys in deps become the keys passed to useFactory, and each value is inferred from its token.

const HTTP = createToken<HttpClient>("HTTP");
provideFactory(HTTP, {
deps: { config: CONFIG },
useFactory: ({ config }) => new HttpClient(config.apiUrl),
});

A factory defaults to singleton scope. Choose another lifetime with scope, and release cached resources with onDispose.

Wrap a token with optional when its provider may be absent. Its inferred value becomes T | undefined, so the missing case stays explicit.

examples/core/optional.ts
import {
createContainer,
createToken,
optional,
provideFactory,
provideValue,
} from "di-craft";
class Logger {
info(message: string): string {
return message;
}
}
class UserService {
private readonly logger: Logger | undefined;
constructor(logger: Logger | undefined) {
this.logger = logger;
}
list(): readonly string[] {
this.logger?.info("list users");
return ["Ada", "Grace"];
}
}
const LOGGER = createToken<Logger>("LOGGER");
const USERS = createToken<UserService>("USERS");
const container = createContainer([
provideFactory(USERS, {
deps: { logger: optional(LOGGER) },
useFactory: ({ logger }) => new UserService(logger),
}),
]);
const users = container.get(USERS);
const logger = container.get(optional(LOGGER));
users.list();
logger?.info("optional logger is registered");
const containerWithLogger = createContainer([
provideValue(LOGGER, new Logger()),
provideFactory(USERS, {
deps: { logger: optional(LOGGER) },
useFactory: ({ logger }) => new UserService(logger),
}),
]);
containerWithLogger.get(USERS).list();

Optional resolution only handles a missing provider. If a registered provider throws while being created, the original error still surfaces.

The token type controls the value accepted by its provider:

import { createToken, provideValue } from "di-craft";
const PORT = createToken<number>("PORT");
// @ts-expect-error A string cannot satisfy Token<number>.
provideValue(PORT, "3000");

Factory dependencies are inferred from their tokens:

import { createToken, provideFactory } from "di-craft";
type Config = { readonly apiUrl: string };
const CONFIG = createToken<Config>("CONFIG");
const API_URL = createToken<string>("API_URL");
provideFactory(API_URL, {
deps: { config: CONFIG },
// @ts-expect-error Config has no "missing" property.
useFactory: ({ config }) => config.missing,
});

Optional dependencies remain optional after resolution:

import { createContainer, createToken, optional } from "di-craft";
type Logger = { info(message: string): void };
const LOGGER = createToken<Logger>("LOGGER");
const container = createContainer();
// @ts-expect-error Optional resolution returns Logger | undefined.
const logger: Logger = container.get(optional(LOGGER));

Next, learn how containers and scopes control ownership and lifetime.