API Reference

Complete reference for all NovaDI APIs with real-world examples

Container

The core dependency injection container that manages service registration and resolution with support for multiple lifetimes, circular dependency detection, and automatic disposal.

resolveType<T>

container.resolveType<T>(typeName?: string): T

Resolves a service by its interface type name. Type name is automatically injected by the transformer at compile-time for zero-runtime overhead.

How It Works

The NovaDI transformer scans your code and automatically injects the interface type name as a string literal at build time. This provides type-safe resolution without runtime reflection or decorators.

Transformer Magic

You write: container.resolveType<ILogger>()

Transformer generates: container.resolveType<ILogger>('ILogger')

The type name is injected at compile-time, giving you both type safety and zero runtime overhead.

Parameters

Name Type Description
typeName Optional string The interface type name (automatically injected by transformer)

Returns

T

The resolved service instance with full TypeScript type inference

Real-World Example

// Service interfaces
interface IEventBus {
  publish(event: string, data: any): void
}

interface ILogger {
  log(message: string): void
}

class EventBus implements IEventBus {
  constructor(private logger: ILogger) {}

  publish(event: string, data: any) {
    this.logger.log(`Event: ${event}`)
    // Publish logic...
  }
}

// Register with automatic transformer-based resolution
const container = new Builder()
  .registerType(ConsoleLogger).as<ILogger>().singleInstance()
  .registerType(EventBus).as<IEventBus>().singleInstance()
  .build()

// ✨ Clean resolution with transformer magic
const eventBus = container.resolveType<IEventBus>()
// Transformer auto-injects: container.resolveType<IEventBus>('IEventBus')

eventBus.publish('user.login', { userId: 123 })

Use Cases

  • Clean resolution: No explicit Token creation needed
  • Type-safe: Full TypeScript inference without decorators
  • Minification-safe: String literals injected at build time, not runtime
  • Refactoring-friendly: Rename interface → transformer updates everywhere

Builder

Fluent API for configuring the container with Autofac-style syntax. Provides a clean, chainable interface for registering services.

registerType<T>

builder.registerType<T>(constructor: new (...args: any[]) => T): RegistrationBuilder<T>

Register a class constructor for dependency injection with automatic autowiring powered by the build-time transformer.

How It Works

When you register a class with registerType, the NovaDI transformer analyzes its constructor parameters at build time and automatically generates resolver functions for each typed dependency. This provides automatic dependency injection without decorators or reflection.

Parameters

Name Type Description
constructor Required new (...args: any[]) => T The class constructor to register

Returns

RegistrationBuilder<T>

A fluent builder for configuring the registration (lifetime, binding, etc.)

Real-World Example

// Service classes
class UserRepository {
  constructor(
    private database: IDatabase,
    private logger: ILogger
  ) {}

  async findById(id: number) {
    this.logger.log(`Finding user ${id}`)
    return this.database.query('SELECT * FROM users WHERE id = ?', [id])
  }
}

class UserService {
  constructor(
    private repository: UserRepository,
    private eventBus: IEventBus
  ) {}

  async getUser(id: number) {
    const user = await this.repository.findById(id)
    this.eventBus.publish('user.fetched', { id })
    return user
  }
}

// Register with automatic autowiring
const container = new Builder()
  .registerType(Database).as<IDatabase>().singleInstance()
  .registerType(ConsoleLogger).as<ILogger>().singleInstance()
  .registerType(EventBus).as<IEventBus>().singleInstance()
  .registerType(UserRepository).singleInstance()  // ✨ Auto-wires database + logger
  .registerType(UserService).singleInstance()     // ✨ Auto-wires repository + eventBus
  .build()

// Everything is wired automatically!
const userService = container.resolveType<UserService>()
const user = await userService.getUser(123)

Automatic Autowiring

The transformer analyzes constructor parameters and generates mapResolvers arrays at build time. This provides O(1) array access performance and is minification-safe. You don't need to call .autoWire() explicitly unless you have primitives that need manual mapping.


AutoWire

Automatic dependency injection strategies. The transformer generates optimal resolution code at build time for zero runtime overhead.

autoWire()

builder.autoWire(options?: AutoWireOptions): this

Configure automatic dependency injection for constructor parameters. Supports two strategies: mapResolvers (transformer-generated, optimal) and map (manual overrides).

How It Works

AutoWire has two strategies with automatic fallback:

  1. mapResolvers (Primary): Transformer-generated array with O(1) access per parameter
  2. map (Fallback): Manual object mapping for edge cases (primitives, environment variables)

When Do You Need .autoWire()?

You don't need it for typed dependencies! The transformer automatically generates mapResolvers for all interface/class types.

You only need it for:

  • Primitive constructor parameters (strings, numbers, booleans)
  • Environment variables and configuration values
  • External constants that aren't DI services

Real-World Example: Mixing Primitives + DI

class ApiClient implements IHttpClient {
  constructor(
    private baseUrl: string,        // ⚠️ Primitive - needs manual mapping
    private apiKey: string,         // ⚠️ Primitive - needs manual mapping
    private logger: ILogger,        // ✅ Typed - transformer handles this
    private retryCount: number      // ⚠️ Primitive - needs manual mapping
  ) {}

  async get<T>(endpoint: string): Promise<T> {
    this.logger.log(`GET ${this.baseUrl}${endpoint}`)
    // HTTP logic...
  }
}

// Register with mixed autowiring
const container = new Builder()
  .registerType(ConsoleLogger).as<ILogger>().singleInstance()
  .registerType(ApiClient).as<IHttpClient>().autoWire({
    map: {
      baseUrl: () => import.meta.env.VITE_API_BASE_URL,
      apiKey: () => import.meta.env.VITE_API_KEY,
      // logger: Transformer already handles this! ✨
      retryCount: () => 3
    }
  }).singleInstance()
  .build()

const client = container.resolveType<IHttpClient>()
await client.get('/users')

Performance Comparison

Strategy Performance Minification-Safe When to Use
mapResolvers ⚡ O(1) array access ✅ Yes Automatic (transformer-generated)
map 🔍 Object property lookup ⚠️ Depends on minifier Primitives & config values

Use Cases

  • Environment variables: API keys, URLs, feature flags from process.env
  • Configuration objects: Port numbers, timeouts, retry counts
  • External constants: Build-time values, version strings
  • Testing mocks: Override specific dependencies for unit tests
  • Polymorphism: Explicit interface selection when conventions don't match
  • Nested interfaces: Specific implementations of base interfaces

Advanced: Explicit Interface Mapping with .map

Sometimes the transformer needs help resolving the correct interface, especially with polymorphism and nested interface hierarchies. Use the map option to explicitly specify which implementation to inject.

When Naming Conventions Don't Match

If your class name doesn't follow the "remove I from interface" convention, the transformer might not find the right registration. Use explicit mapping to tell it exactly which interface to resolve.

// Example 1: Naming convention mismatch
interface IPaymentProcessor {
  processPayment(amount: number): Promise<void>
}

interface INotificationService {
  notify(message: string): void
}

// ❌ Class name doesn't match IPaymentProcessor convention
class StripePaymentGateway implements IPaymentProcessor {
  constructor(private notificationService: INotificationService) {}

  async processPayment(amount: number) {
    // Process with Stripe...
    this.notificationService.notify(`Processed $${amount}`)
  }
}

// ✅ Explicit mapping tells transformer which interface to use
const container = new Builder()
  .registerType(EmailNotificationService).as<INotificationService>().singleInstance()
  .registerType(StripePaymentGateway)
    .as<IPaymentProcessor>()
    .autoWire({
      map: {
        // Explicitly resolve INotificationService (transformer can't infer from parameter name)
        notificationService: (c) => c.resolveType<INotificationService>()
      }
    })
    .singleInstance()
  .build()

Polymorphism: Selecting Specific Implementations

When working with interface hierarchies (e.g., IMonkey implements IAnimal), you often want to inject a specific implementation rather than just any IAnimal. This is common in polymorphic designs.

// Interface hierarchy
interface IAnimal {
  makeSound(): string
}

interface IMonkey extends IAnimal {
  climb(): void
}

interface IElephant extends IAnimal {
  trumpet(): void
}

// Implementations
class Monkey implements IMonkey {
  makeSound() { return 'Ooh ooh ah ah!' }
  climb() { console.log('🌴 Climbing tree...') }
}

class Elephant implements IElephant {
  makeSound() { return 'Paaooo!' }
  trumpet() { console.log('🎺 Trumpeting...') }
}

// Zoo needs a SPECIFIC animal, not just any IAnimal
class MonkeyZoo {
  constructor(
    private animal: IAnimal  // ⚠️ Parameter type is IAnimal, but we want IMonkey specifically!
  ) {}

  performShow() {
    console.log(this.animal.makeSound())
    // We know it's a monkey, so we want to call climb()
    if ('climb' in this.animal) {
      (this.animal as IMonkey).climb()
    }
  }
}

// ✅ Explicit mapping: Inject IMonkey even though parameter type is IAnimal
const container = new Builder()
  .registerType(Monkey).as<IMonkey>().singleInstance()
  .registerType(Elephant).as<IElephant>().singleInstance()
  .registerType(MonkeyZoo)
    .autoWire({
      map: {
        // Explicitly resolve IMonkey, not generic IAnimal
        animal: (c) => c.resolveType<IMonkey>()
      }
    })
    .singleInstance()
  .build()

const zoo = container.resolveType<MonkeyZoo>()
zoo.performShow()  // 🐵 "Ooh ooh ah ah!" + climbing

Why Not Just Use IMonkey as Parameter Type?

Design flexibility! Sometimes you want the class to accept any IAnimal for flexibility, but configure it with a specific implementation at registration time. This is the Strategy Pattern in action.

Other times, the parameter type is defined in a third-party library you can't change, so explicit mapping is your only option.

Multiple Registrations: Choosing Between Implementations

When you have multiple implementations of the same interface, use map to explicitly select which one to inject.

interface ILogger {
  log(message: string): void
}

class ConsoleLogger implements ILogger {
  log(msg: string) { console.log(msg) }
}

class FileLogger implements ILogger {
  log(msg: string) { /* write to file */ }
}

class CloudLogger implements ILogger {
  log(msg: string) { /* send to cloud */ }
}

// Service needs TWO different loggers for different purposes
class PaymentService {
  constructor(
    private auditLogger: ILogger,      // Should be FileLogger for compliance
    private debugLogger: ILogger       // Should be ConsoleLogger for dev
  ) {}
}

// ✅ Explicit mapping: Different ILogger implementations for different parameters
const container = new Builder()
  .registerType(ConsoleLogger).as<ILogger>().named('console').singleInstance()
  .registerType(FileLogger).as<ILogger>().named('file').singleInstance()
  .registerType(PaymentService)
    .autoWire({
      map: {
        auditLogger: (c) => c.resolveTypeNamed<ILogger>('file'),
        debugLogger: (c) => c.resolveTypeNamed<ILogger>('console')
      }
    })
    .singleInstance()
  .build()

AutoWireOptions

Interface
interface AutoWireOptions

Configuration options for automatic dependency injection behavior.

Properties

Property Type Description
strict Optional boolean Enable strict mode: throw error if dependency cannot be resolved. Default: false
map Optional Record<string, (c: Container) => any> Manual mapping object with parameter names as keys and resolver functions as values. Used for primitives, environment variables, and explicit interface selection.
mapResolvers Optional Array<((c: Container) => any) | undefined> Array of resolver functions in parameter position order. Automatically generated by the transformer for optimal performance (O(1) array access).
by Optional 'name' | 'type' Resolution strategy: 'name' for parameter name matching, 'type' for type-based resolution (default). Transformer uses type-based resolution for reliability.

Example: Combining All Options

class DatabaseService {
  constructor(
    private connectionString: string,
    private logger: ILogger,
    private maxRetries: number
  ) {}
}

const container = new Builder()
  .registerType(ConsoleLogger).as<ILogger>().singleInstance()
  .registerType(DatabaseService)
    .autoWire({
      strict: true,  // Throw error if logger is not registered
      map: {
        connectionString: () => process.env.DB_CONNECTION_STRING,
        maxRetries: () => 3
        // logger is handled by transformer automatically via mapResolvers
      },
      by: 'type'  // Use type-based resolution (default)
    })
    .singleInstance()
  .build()

Advanced Techniques

Advanced autowiring patterns for complex scenarios: factory resolution, conditional injection, scope-aware dependencies, and dynamic configuration.

1. Factory-Based Resolution

Use factory functions in map to create complex dependencies or add custom logic during resolution.

// Complex configuration object factory
interface IConfig {
  apiUrl: string
  timeout: number
  retryStrategy: 'exponential' | 'linear'
}

class ApiService {
  constructor(
    private config: IConfig,
    private logger: ILogger
  ) {}
}

const container = new Builder()
  .registerType(ConsoleLogger).as<ILogger>().singleInstance()
  .registerType(ApiService)
    .autoWire({
      map: {
        // Factory creates config based on environment
        config: () => ({
          apiUrl: import.meta.env.PROD
            ? 'https://api.production.com'
            : 'http://localhost:3000',
          timeout: import.meta.env.PROD ? 10000 : 30000,
          retryStrategy: import.meta.env.PROD ? 'exponential' : 'linear'
        })
      }
    })
    .singleInstance()
  .build()

2. Conditional Injection Based on Environment

Inject different implementations based on runtime conditions like environment, feature flags, or configuration.

interface ICache {
  get(key: string): Promise<any>
  set(key: string, value: any): Promise<void>
}

class RedisCache implements ICache {
  async get(key: string) { /* Redis implementation */ }
  async set(key: string, value: any) { /* Redis implementation */ }
}

class InMemoryCache implements ICache {
  async get(key: string) { /* In-memory implementation */ }
  async set(key: string, value: any) { /* In-memory implementation */ }
}

class UserService {
  constructor(private cache: ICache) {}
}

const container = new Builder()
  .registerType(RedisCache).as<ICache>().named('redis').singleInstance()
  .registerType(InMemoryCache).as<ICache>().named('memory').singleInstance()
  .registerType(UserService)
    .autoWire({
      map: {
        // Conditional: Use Redis in production, in-memory for dev/test
        cache: (c) => {
          const env = import.meta.env.MODE
          return env === 'production'
            ? c.resolveTypeNamed<ICache>('redis')
            : c.resolveTypeNamed<ICache>('memory')
        }
      }
    })
    .singleInstance()
  .build()

3. Scope-Aware Dependencies

Create child containers and resolve scoped dependencies for request-specific or tenant-specific contexts.

interface IRequestContext {
  userId: string
  tenantId: string
}

class RequestContext implements IRequestContext {
  constructor(
    public userId: string,
    public tenantId: string
  ) {}
}

class TenantDatabase {
  constructor(
    private context: IRequestContext,
    private logger: ILogger
  ) {
    this.logger.log(`DB for tenant: ${context.tenantId}`)
  }

  query(sql: string) {
    // Use context.tenantId to route to correct database
  }
}

// Global container with singletons
const rootContainer = new Builder()
  .registerType(ConsoleLogger).as<ILogger>().singleInstance()
  .registerType(TenantDatabase).scoped()  // Scoped to child container
  .build()

// Per-request child container
function handleRequest(userId: string, tenantId: string) {
  const requestScope = rootContainer.createChild()

  // Register request-specific context
  requestScope.bindValue(
    { symbol: Symbol('IRequestContext') } as any,
    new RequestContext(userId, tenantId),
    { lifetime: 'scoped' }
  )

  // TenantDatabase gets scoped context + singleton logger
  const db = requestScope.resolveType<TenantDatabase>()
  db.query('SELECT * FROM users')

  requestScope.dispose()  // Clean up scoped instances
}

4. Lazy Resolution with Factories

Defer dependency creation until it's actually needed using factory injection.

interface IHeavyService {
  process(): void
}

class HeavyService implements IHeavyService {
  constructor() {
    console.log('⚠️ Expensive initialization!')
  }
  process() { /* Heavy work */ }
}

// Instead of injecting the service directly, inject a factory
type ServiceFactory<T> = () => T

class OptimizedController {
  constructor(
    private serviceFactory: ServiceFactory<IHeavyService>,
    private logger: ILogger
  ) {}

  handleRequest(needsHeavyProcessing: boolean) {
    this.logger.log('Request received')

    if (needsHeavyProcessing) {
      // Only create HeavyService when actually needed!
      const service = this.serviceFactory()
      service.process()
    }
  }
}

const container = new Builder()
  .registerType(ConsoleLogger).as<ILogger>()
  .registerType(HeavyService).as<IHeavyService>().instancePerDependency()
  .registerType(OptimizedController)
    .autoWire({
      map: {
        // Inject a factory function instead of the service itself
        serviceFactory: (c) => () => c.resolveType<IHeavyService>()
      }
    })
  .build()

const controller = container.resolveType<OptimizedController>()
controller.handleRequest(false)  // ✅ No heavy initialization!
controller.handleRequest(true)   // ⚠️ Now it initializes

5. Decorator Pattern with DI

Wrap services with decorators while maintaining dependency injection.

interface IDataService {
  getData(id: string): Promise<any>
}

class DataService implements IDataService {
  async getData(id: string) {
    // Fetch from database
    return { id, data: 'raw data' }
  }
}

// Decorator: Adds caching
class CachedDataService implements IDataService {
  private cache = new Map()

  constructor(
    private inner: IDataService,
    private logger: ILogger
  ) {}

  async getData(id: string) {
    if (this.cache.has(id)) {
      this.logger.log(`Cache hit: ${id}`)
      return this.cache.get(id)
    }

    const data = await this.inner.getData(id)
    this.cache.set(id, data)
    return data
  }
}

// Decorator: Adds logging
class LoggedDataService implements IDataService {
  constructor(
    private inner: IDataService,
    private logger: ILogger
  ) {}

  async getData(id: string) {
    this.logger.log(`Getting data: ${id}`)
    const result = await this.inner.getData(id)
    this.logger.log(`Got data: ${id}`)
    return result
  }
}

const container = new Builder()
  .registerType(ConsoleLogger).as<ILogger>()
  .registerType(DataService).named('core').instancePerDependency()

  // Wrap with caching decorator
  .registerType(CachedDataService)
    .named('cached')
    .autoWire({
      map: {
        inner: (c) => c.resolveTypeNamed<IDataService>('core')
      }
    })
    .instancePerDependency()

  // Wrap cached version with logging decorator
  .registerType(LoggedDataService)
    .as<IDataService>()  // This is the final service
    .autoWire({
      map: {
        inner: (c) => c.resolveTypeNamed<IDataService>('cached')
      }
    })
    .singleInstance()
  .build()

// Resolves: LoggedDataService → CachedDataService → DataService
const service = container.resolveType<IDataService>()
await service.getData('123')  // Logs + caches + fetches

6. Dynamic Configuration with Feature Flags

Enable/disable features dynamically at registration time.

interface IAnalytics {
  track(event: string): void
}

class GoogleAnalytics implements IAnalytics {
  track(event: string) {
    console.log(`📊 GA: ${event}`)
  }
}

class MixpanelAnalytics implements IAnalytics {
  track(event: string) {
    console.log(`📈 Mixpanel: ${event}`)
  }
}

class NoOpAnalytics implements IAnalytics {
  track(event: string) {
    // Do nothing (analytics disabled)
  }
}

// Feature flags from environment
const features = {
  analyticsProvider: import.meta.env.VITE_ANALYTICS_PROVIDER, // 'google' | 'mixpanel' | 'none'
  enablePremiumFeatures: import.meta.env.VITE_PREMIUM_FEATURES === 'true'
}

const container = new Builder()
  .registerType(GoogleAnalytics).named('google').singleInstance()
  .registerType(MixpanelAnalytics).named('mixpanel').singleInstance()
  .registerType(NoOpAnalytics).named('none').singleInstance()

  // Main app gets analytics based on feature flag
  .registerType(App)
    .autoWire({
      map: {
        analytics: (c) => {
          switch (features.analyticsProvider) {
            case 'google': return c.resolveTypeNamed<IAnalytics>('google')
            case 'mixpanel': return c.resolveTypeNamed<IAnalytics>('mixpanel')
            default: return c.resolveTypeNamed<IAnalytics>('none')
          }
        },
        premiumEnabled: () => features.enablePremiumFeatures
      }
    })
    .singleInstance()
  .build()

Best Practices

  • Keep it simple: Only use advanced techniques when you have a real need
  • Transformer first: Let the transformer handle typed dependencies automatically
  • Use named registrations: For multiple implementations of the same interface
  • Factory pattern: For lazy initialization and dynamic dependencies
  • Decorator pattern: For cross-cutting concerns (logging, caching, validation)
  • Test your factories: Complex map functions should have unit tests

API Overview

Complete overview of all NovaDI public methods. Methods marked with ✅ are transformer-based and recommended for use. Methods marked with ⚠️ are token-based low-level APIs used internally.

Container Resolution

Method Description
resolveType<T>(typeName?: string) Primary resolution method. Transformer injects interface name automatically.
resolveTypeNamed<T>(name: string, typeName?: string) Resolve named registration. Transformer-compatible.
resolveTypeKeyed<T>(key: any, typeName?: string) Resolve keyed registration. Transformer-compatible.
resolveTypeAll<T>(typeName?: string) Resolve all registrations for a type. Transformer-compatible. NEW: Automatically used for array injection (IFoo[], Array<IFoo>) in constructors.
⚠️ resolve<T>(token: Token<T>) Low-level resolution with explicit Token. Used internally.
⚠️ resolveNamed<T>(token: Token<T>, name: string) Low-level named resolution with Token.
⚠️ resolveKeyed<T>(token: Token<T>, key: any) Low-level keyed resolution with Token.
⚠️ resolveAll<T>(token: Token<T>) Low-level batch resolution with Token.

Builder Registration

Method Description
registerType<T>(constructor) Register a class with automatic transformer-based autowiring.
registerInstance<T>(instance) Register an existing instance. Transformer-compatible.
registerFactory<T>(factory) Register a factory function. Transformer-compatible.
🔧 build() Build and return the configured container.

RegistrationBuilder (Fluent Chain)

Method Description
.as<U>(typeName?: string) Bind to interface type. Transformer injects type name.
.asSelf() Register as concrete type (Autofac-style AsSelf). Uses constructor.name.
.named(name: string) Register with a named identifier.
.keyed(key: any) Register with a keyed identifier.
.autoWire(options?) Configure automatic dependency injection. Transformer generates resolvers.
🔧 .singleInstance() Set lifetime to singleton (DEFAULT - no call needed).
🔧 .instancePerDependency() Set lifetime to transient (new instance each time).
🔧 .instancePerRequest() Set lifetime to per-request (one instance per resolution tree).

Container Management

Method Description
🔧 createChild() Create a child container (for scoped resolution).
🔧 dispose() Dispose all disposable services in the container.
⚠️ bindValue<T>(token, value, options?) Low-level manual value binding. Used internally by Builder.
⚠️ bindFactory<T>(token, factory, options?) Low-level manual factory binding. Used internally by Builder.
⚠️ bindClass<T>(token, constructor, options?) Low-level manual class binding. Used internally by Builder.

Token API (Low-Level)

Method Description
⚠️ Token<T>(description?: string) Create a type-safe token. Used internally. Not needed with transformer.

Legend

  • ✅ Transformer-based: Recommended methods that work with the NovaDI transformer for automatic type injection
  • ⚠️ Token-based: Low-level methods requiring manual Token management. Used internally by the framework.
  • 🔧 Utility: Helper methods for configuration and lifecycle management

Error Classes

Specialized error types for dependency injection failures with helpful debugging information.