Tokens and providers
Tokens
Section titled “Tokens”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); // ConfigValue providers
Section titled “Value providers”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.
Factory providers
Section titled “Factory providers”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.
Optional dependencies
Section titled “Optional dependencies”Wrap a token with optional when its provider may be absent. Its inferred value
becomes T | undefined, so the missing case stays explicit.
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.
What TypeScript rejects
Section titled “What TypeScript rejects”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.