Libraries
    Preparing search index...

    Module @cleverbrush/di

    @cleverbrush/di — .NET-style dependency injection for TypeScript.

    Uses @cleverbrush/schema instances as service keys for type-safe registration and resolution. Supports singleton, scoped, and transient lifetimes, function injection via FunctionSchemaBuilder, and automatic disposal of scoped services.

    import { ServiceCollection } from '@cleverbrush/di';
    import { object, string, number, func } from '@cleverbrush/schema';

    // Define service contracts as schemas
    const IConfig = object({ port: number(), host: string() });
    const ILogger = object({ info: func().addParameter(string()) });

    // Register services
    const services = new ServiceCollection();
    services.addSingleton(IConfig, { port: 3000, host: 'localhost' });
    services.addSingleton(ILogger, () => ({ info: console.log }));

    // Build the provider
    const provider = services.buildServiceProvider();

    // Resolve — fully typed, no generics needed
    const config = provider.get(IConfig);
    config.port; // number

    @cleverbrush/di

    CI License: BSD-3-Clause

    A .NET-style dependency injection container for TypeScript. Uses @cleverbrush/schema instances as service keys for fully-typed, zero-generic registration and resolution. Supports three service lifetimes, schema-driven function injection, and automatic disposal of scoped services.

    • Schema-as-key — schema instances (from @cleverbrush/schema) serve as service identifiers via reference equality. No string tokens, no Symbol, no decorators.
    • Three lifetimesSingleton, Scoped, and Transient, matching .NET semantics.
    • Function injection — resolve dependencies automatically via FunctionSchemaBuilder parameter lists with invoke() / addSingletonFromSchema().
    • Scope validation — resolving a scoped service from the root provider throws by default, catching common lifecycle bugs early.
    • Automatic disposalServiceScope disposes scoped services in reverse creation order (LIFO) on scope exit, supporting both Symbol.dispose and Symbol.asyncDispose.
    • using keyword supportServiceScope implements Disposable and AsyncDisposable.
    • No decorators, no reflect-metadata — works in any TypeScript project without experimentalDecorators.
    • Zero runtime dependencies (other than @cleverbrush/schema).
    npm install @cleverbrush/di @cleverbrush/schema
    
    import { ServiceCollection } from '@cleverbrush/di';
    import { object, string, number, func } from '@cleverbrush/schema';

    // 1. Define service contracts as schemas (used as keys)
    const IConfig = object({ port: number(), host: string() });
    const ILogger = object({ info: func().addParameter(string()) });

    // 2. Register services
    const services = new ServiceCollection();
    services.addSingleton(IConfig, { port: 3000, host: 'localhost' });
    services.addSingleton(ILogger, () => ({ info: (msg) => console.log(msg) }));

    // 3. Build the provider
    const provider = services.buildServiceProvider();

    // 4. Resolve — fully typed, no explicit generics needed
    const config = provider.get(IConfig);
    config.port; // number
    Lifetime Created Use for
    Singleton Once per ServiceProvider Loggers, configuration, connection pools
    Scoped Once per ServiceScope Database contexts, HTTP request state
    Transient On every get() call Lightweight, stateless services
    const IDbContext = object({ query: func() });
    const IRequestId = object({ id: string() });

    services.addSingleton(IConfig, { port: 3000, host: 'localhost' });
    services.addScoped(IDbContext, (provider) => {
    const config = provider.get(IConfig);
    return new DbContext(config.host);
    });
    services.addTransient(IRequestId, () => ({ id: crypto.randomUUID() }));
    services.addSingleton(IConfig, { port: 3000, host: 'localhost' });
    

    The factory receives the ServiceProvider so it can resolve other services:

    services.addSingleton(ILogger, (provider) => {
    const config = provider.get(IConfig);
    return new ConsoleLogger(config.logLevel);
    });

    Use when the service is a function type — addSingleton would invoke a function value as a factory, but addSingletonInstance always treats its argument as the value:

    const IHandler = func();
    const myHandler = (req: Request) => new Response('ok');

    services.addSingletonInstance(IHandler, myHandler);

    Declare dependencies via a FunctionSchemaBuilder and let the container resolve them automatically:

    const IGreeter = object({ greet: func() });

    const greeterDeps = func()
    .addParameter(IConfig)
    .addParameter(ILogger);

    services.addSingletonFromSchema(
    IGreeter,
    greeterDeps,
    (config, logger) => ({
    greet() { logger.info(`Hello from ${config.host}`); }
    })
    );
    // Also available: addScopedFromSchema, addTransientFromSchema

    Pass { validate: true } to any registration method to validate the resolved value against its schema at resolution time. Useful in development and testing:

    services.addSingleton(IConfig, loadConfig(), { validate: true });
    
    const provider = services.buildServiceProvider();

    // Throws if not registered
    const config = provider.get(IConfig);

    // Returns undefined if not registered (no throw)
    const mailer = provider.getOptional(IMailer);

    Create a scope for work that has a bounded lifetime (e.g. an HTTP request). Scoped services are cached within the scope and disposed when the scope exits.

    // Automatic disposal with `using`
    using scope = provider.createScope();
    const db = scope.serviceProvider.get(IDbContext);
    // db is disposed when the block exits

    // Async disposal
    await using scope = provider.createScope();
    const db = scope.serviceProvider.get(IDbContext);
    // db.[Symbol.asyncDispose]() is called when the block exits

    // Manual disposal
    const scope = provider.createScope();
    try {
    const db = scope.serviceProvider.get(IDbContext);
    // use db...
    } finally {
    await scope.asyncDispose();
    }

    Services registered with a class that implements Symbol.dispose or Symbol.asyncDispose are tracked and disposed automatically when the scope ends.

    Resolve dependencies described by a FunctionSchemaBuilder and call a function with them directly, without registering anything:

    const handlerDeps = func()
    .addParameter(ILogger)
    .addParameter(IDbContext);

    const result = scope.serviceProvider.invoke(handlerDeps, (logger, db) => {
    logger.info('Handling request');
    return db.query('SELECT 1');
    });

    By default, resolving a Scoped service from the root provider throws an error, preventing accidental singleton captures:

    // Throws: "Cannot resolve scoped service from the root provider."
    provider.get(IDbContext);

    // Disable if intentional (not recommended for production)
    const provider = services.buildServiceProvider({ validateScopes: false });

    The container detects circular dependencies at resolution time and throws a descriptive error showing the dependency chain.

    Method Description
    addSingleton(schema, factoryOrValue, options?) Register a singleton service
    addScoped(schema, factory, options?) Register a scoped service
    addTransient(schema, factory, options?) Register a transient service
    addSingletonInstance(schema, instance, options?) Register a pre-created singleton (safe for function values)
    addSingletonFromSchema(target, funcSchema, impl, options?) Register a singleton with schema-driven dependencies
    addScopedFromSchema(target, funcSchema, impl, options?) Register a scoped service with schema-driven dependencies
    addTransientFromSchema(target, funcSchema, impl, options?) Register a transient service with schema-driven dependencies
    buildServiceProvider(options?) Build the immutable ServiceProvider
    Method Description
    get(schema) Resolve a service; throws if not registered
    getOptional(schema) Resolve a service; returns undefined if not registered
    createScope() Create a new ServiceScope
    invoke(funcSchema, implementation) Call a function with its dependencies resolved automatically
    Member Description
    serviceProvider The scoped IServiceProvider for this scope
    [Symbol.dispose]() Synchronously dispose scoped services (LIFO)
    [Symbol.asyncDispose]() Asynchronously dispose scoped services (LIFO)
    asyncDispose() Explicit async dispose (same as Symbol.asyncDispose)

    BSD 3-Clause

    Enumerations

    ServiceLifetime

    Classes

    ScopedServiceProvider
    ServiceCollection
    ServiceProvider
    ServiceScope

    Interfaces

    IServiceProvider
    ServiceDescriptor
    ServiceProviderOptions
    ServiceRegistrationOptions

    Type Aliases

    ServiceFactory

    Functions

    isAsyncDisposable
    isDisposable