Docs/Dependency injectionv1.0.0-rc.1

Providers — API Reference

@tagna/udiot v1.0.0-rc.1


Overview

A provider is a plain object that tells the container what token to register and how to produce the corresponding value. All provider interfaces share a provide field (the token) and an optional scope field. The distinguishing field (useClass, useValue, useFactory, or useExisting) determines the production strategy.

import type {
  ClassProvider,
  ValueProvider,
  FactoryProvider,
  ExistingProvider,
  AnyProvider
} from '@tagna/udiot';

ServiceScope Enum

enum ServiceScope {
  SINGLETON = 'singleton',
  TRANSIENT = 'transient',
  SCOPED    = 'scoped'
}

Applies to ClassProvider and FactoryProvider. ValueProvider and ExistingProvider have no scope — the same value or alias is always returned.

Value Meaning
SINGLETON One instance per container. Default for registerClass().
TRANSIENT New instance on every resolve().
SCOPED One instance per child container created with createChild().

ClassProvider

interface ClassProvider<T = any> {
  provide: InjectionToken;
  useClass: new (...args: any[]) => T;
  scope?: ServiceScope;
}

The container reads @Injectable metadata from useClass to discover constructor dependencies, resolves each one recursively, and calls new useClass(...resolvedDeps).

The class must be decorated with @Injectable(). Attempting to register a plain (undecorated) class as a ClassProvider will throw InvalidProviderError at resolution time.

import { ServiceScope } from '@tagna/udiot';

container.register({
  provide: UserService,
  useClass: UserService,
  scope: ServiceScope.SINGLETON
});

// Shorthand
container.registerClass(UserService, UserService);

ValueProvider

interface ValueProvider<T = any> {
  provide: InjectionToken;
  useValue: T;
  // no scope — always returns the same value
}

Useful for:

  • Configuration objects assembled from environment variables.
  • Third-party library instances that you create yourself.
  • String or numeric constants.
container.register({
  provide: 'DB_CONFIG',
  useValue: {
    host: process.env.DB_HOST ?? 'localhost',
    port: Number(process.env.DB_PORT ?? 5432),
    database: 'myapp'
  }
});

// Shorthand
container.registerValue('DB_CONFIG', { host: 'localhost', port: 5432, database: 'myapp' });

FactoryProvider

interface FactoryProvider<T = any> {
  provide: InjectionToken;
  useFactory: (...args: any[]) => T;
  inject?: InjectionToken[];
  scope?: ServiceScope;
}

inject is an ordered array of tokens. The container resolves each token and passes the results as positional arguments to useFactory. The factory can be synchronous.

Use factories when:

  • Construction involves logic that cannot live in a class constructor.
  • You need to call a builder or third-party factory function.
  • Instance creation conditionally differs based on resolved dependencies.
container.register({
  provide: ConnectionPool,
  useFactory: (config: DbConfig) => {
    return new ConnectionPool(config.host, config.port, {
      max: 10,
      idleTimeoutMillis: 30_000
    });
  },
  inject: ['DB_CONFIG'],
  scope: ServiceScope.SINGLETON
});

// Shorthand (inject array is the third parameter)
container.registerFactory(
  ConnectionPool,
  (config: DbConfig) => new ConnectionPool(config.host, config.port),
  ['DB_CONFIG']
);

ExistingProvider

interface ExistingProvider<T = any> {
  provide: InjectionToken;
  useExisting: InjectionToken;
  // no scope — delegates to the aliased token's provider
}

Makes one token resolve to whatever another token resolves to. The aliased token must be registered independently.

// Register the concrete implementation
container.registerClass(EmailService, EmailService);

// Alias the interface string token to the concrete class
container.register({
  provide: 'IEmailService',
  useExisting: EmailService
});

// Both tokens now resolve the same EmailService instance
container.resolve('IEmailService') === container.resolve(EmailService); // true

AnyProvider Union Type

type AnyProvider<T = any> =
  | ClassProvider<T>
  | ValueProvider<T>
  | FactoryProvider<T>
  | ExistingProvider<T>;

Used by container.register() and container.registerMany().


Type Guards

The package exports runtime type guards for discriminating provider objects:

import {
  isClassProvider,
  isValueProvider,
  isFactoryProvider,
  isExistingProvider,
  normalizeProvider
} from '@tagna/udiot';

isClassProvider({ provide: Foo, useClass: Foo });         // true
isValueProvider({ provide: 'X', useValue: 42 });          // true
isFactoryProvider({ provide: Foo, useFactory: () => {} }); // true
isExistingProvider({ provide: 'Alias', useExisting: Foo }); // true

normalizeProvider(value) accepts either a provider object or a bare class constructor and always returns a full AnyProvider. It is used internally by container.register().


When to Use Each Provider Type

Situation Provider
Registering your own service class ClassProvider
Providing a config object or env var ValueProvider
Third-party class that doesn't use @Injectable FactoryProvider
Construction needs conditional logic or builder pattern FactoryProvider
Exposing a class under an interface string token ExistingProvider
Mocking a service in tests ValueProvider (inject the mock instance)

Complete Example: Database Configuration Pattern

This example registers a config object with ValueProvider, the service class with ClassProvider, and a third-party ConnectionPool with FactoryProvider.

import { Container, ServiceScope } from '@tagna/udiot';
import { Injectable } from '@tagna/udiot';

// --- Types ---
interface DbConfig {
  host: string;
  port: number;
  database: string;
  poolSize: number;
}

// --- Services ---
@Injectable({ scope: ServiceScope.SINGLETON })
class DatabaseService {
  constructor(private pool: ConnectionPool) {}

  async query(sql: string): Promise<any[]> {
    const client = await this.pool.connect();
    try {
      return await client.query(sql);
    } finally {
      client.release();
    }
  }
}

// --- Registration ---
const container = new Container();

// 1. ValueProvider — config from environment
container.register({
  provide: 'DB_CONFIG',
  useValue: {
    host: process.env.DB_HOST ?? 'localhost',
    port: 5432,
    database: 'myapp',
    poolSize: 10
  } satisfies DbConfig
});

// 2. FactoryProvider — third-party pool, depends on config
container.register({
  provide: ConnectionPool,
  useFactory: (config: DbConfig) =>
    new ConnectionPool(`postgres://${config.host}:${config.port}/${config.database}`, {
      max: config.poolSize
    }),
  inject: ['DB_CONFIG'],
  scope: ServiceScope.SINGLETON
});

// 3. ClassProvider — own service, depends on pool (resolved automatically)
container.registerClass(DatabaseService, DatabaseService);

// --- Resolution ---
const db = container.resolve<DatabaseService>(DatabaseService);
const rows = await db.query('SELECT 1');
From the canonical UDIOT public documentation.Try it in the playground ↗