Skip to main content

Providers

Providers tell the dependency injection system how to create and supply dependencies. They are the foundation that makes dependency injection work, defining how services, values, and factories are registered and resolved.

What are Providers?

A provider is a recipe that tells the DI system how to create an instance of a dependency. When a service requests a dependency, the DI system uses the appropriate provider to create or retrieve that dependency.

Provider Concept

// When this service is created...
@TpService()
class UserService {
constructor(private db: DatabaseService) {} // How does DI get DatabaseService?
}

// The answer is: through a provider
platform.import(DatabaseService) // This creates a ClassProvider for DatabaseService

Types of Providers

Tarpit supports several types of providers for different use cases:

1. ClassProvider

The most common provider type - tells DI to create instances using a class constructor.

ClassProvider has two definition forms:

Shorthand Form (Recommended)

@TpService()
class DatabaseService {
connect() {
console.log('Connected to database')
}
}

// Shorthand: directly pass the class
// Equivalent to { provide: DatabaseService, useClass: DatabaseService }
platform.import(DatabaseService)

Explicit Object Form

// Explicit form: when token and implementation are the same
platform.import({
provide: DatabaseService,
useClass: DatabaseService
})

// Explicit form: when token and implementation are different
platform.import({
provide: 'database-service', // Token to inject with
useClass: DatabaseService // Implementation class
})

// Explicit form: interface-based injection
interface PaymentProcessor {
process(amount: number): Promise<void>
}

@TpService()
class StripePaymentProcessor implements PaymentProcessor {
async process(amount: number) {
// Stripe implementation
}
}

platform.import({
provide: 'PaymentProcessor', // String token
useClass: StripePaymentProcessor // Concrete implementation
})

Usage Examples

@TpService()
class UserService {
constructor(
private db: DatabaseService, // Injected via shorthand form
@Inject('database-service') private db2: DatabaseService, // Injected via string token
@Inject('PaymentProcessor') private payment: PaymentProcessor // Injected via interface
) {}
}

2. ValueProvider

Provides a pre-existing value or object:

// Simple value
platform.import({
provide: 'app-name',
useValue: 'My Awesome App'
})

// Configuration object
platform.import({
provide: 'database-config',
useValue: {
host: 'localhost',
port: 5432,
database: 'myapp'
}
})

// Using with injection
@TpService()
class DatabaseService {
constructor(
@Inject('database-config') private config: any
) {}
}

3. FactoryProvider

Uses a function to create the dependency:

// Simple factory
platform.import({
provide: 'timestamp',
useFactory: () => Date.now()
})

// Factory with dependencies
platform.import({
provide: 'database-connection',
useFactory: (config: any) => {
return new Database(config.host, config.port)
},
deps: ['database-config'] // Dependencies for the factory
})

// Complex factory
platform.import({
provide: 'logger',
useFactory: (config: AppConfig) => {
if (config.debug) {
return new ConsoleLogger()
} else {
return new FileLogger('/var/log/app.log')
}
},
deps: [TpConfigData]
})

Provider Registration

Using .import()

The .import() method accepts various provider formats:

// Class (creates ClassProvider)
platform.import(UserService)

// Import multiple services individually
platform.import(UserService)
platform.import(OrderService)
platform.import(PaymentService)

// Explicit provider object
platform.import({
provide: UserService,
useClass: UserService
})

// Import multiple providers individually
platform.import(UserService)
platform.import({ provide: 'api-key', useValue: 'secret-key' })
platform.import({ provide: 'database', useFactory: () => new Database() })

Module Providers

Modules can declare their own providers:

@TpModule({
providers: [
UserService,
{ provide: 'api-url', useValue: 'https://api.example.com' },
{
provide: 'http-client',
useFactory: (url: string) => new HttpClient(url),
deps: ['api-url']
}
]
})
class ApiModule {}

Advanced Provider Patterns

Conditional Providers

Use factories to provide different implementations based on conditions:

// Define interface
abstract class PaymentProcessor {
abstract process(amount: number): Promise<void>
}

// Implementations
@TpService()
class StripeProcessor extends PaymentProcessor {
async process(amount: number) {
// Stripe implementation
}
}

@TpService()
class PayPalProcessor extends PaymentProcessor {
async process(amount: number) {
// PayPal implementation
}
}

// Conditional provider
platform.import({
provide: PaymentProcessor,
useFactory: (config: TpConfigData) => {
const paymentProvider = config.get('payment.provider')
if (paymentProvider === 'stripe') {
return new StripeProcessor()
} else {
return new PayPalProcessor()
}
},
deps: [TpConfigData]
})

Multi-Providers

Provide multiple values for the same token:

// Define token
const PLUGIN_TOKEN = Symbol('PLUGINS')

// Register multiple providers
platform.import({ provide: PLUGIN_TOKEN, useValue: new AuthPlugin(), multi: true })
platform.import({ provide: PLUGIN_TOKEN, useValue: new LoggingPlugin(), multi: true })
platform.import({ provide: PLUGIN_TOKEN, useValue: new CachePlugin(), multi: true })

// Inject all providers as an array
@TpService()
class PluginManager {
constructor(@Inject(PLUGIN_TOKEN) private plugins: Plugin[]) {
// plugins is an array of all registered plugins
}
}

Best Practices

1. Use Descriptive Tokens

Use clear, descriptive tokens for your providers:

// ✅ Good - Clear and descriptive
const DATABASE_CONNECTION_STRING = Symbol('DATABASE_CONNECTION_STRING')
const HTTP_CLIENT_TIMEOUT = Symbol('HTTP_CLIENT_TIMEOUT')

// ❌ Avoid - Vague or confusing
const CONFIG = Symbol('CONFIG')
const THING = Symbol('THING')

2. Prefer Class Providers

Use class providers when possible for better type safety:

// ✅ Good - Type-safe class provider
@TpService()
class EmailService {
send(to: string, message: string) { /* ... */ }
}

// ❌ Less ideal - Untyped value provider
platform.import({
provide: 'email-service',
useValue: {
send: (to: string, message: string) => { /* ... */ }
}
})

3. Keep Factories Simple

Keep factory functions focused and testable:

// ✅ Good - Simple, focused factory
platform.import({
provide: 'logger',
useFactory: (config: TpConfigData) => {
const debug = config.get('debug') ?? false
return debug ? new ConsoleLogger() : new FileLogger()
},
deps: [TpConfigData]
})

// ❌ Avoid - Complex factory with side effects
platform.import({
provide: 'complex-service',
useFactory: (config: TpConfigData) => {
// Too much logic in factory
const service = new ComplexService()
service.configure(config.get())
service.loadPlugins()
service.initializeDatabase()
return service
},
deps: [TpConfigData]
})

4. Use Interfaces for Abstraction

Define interfaces for better abstraction:

// Define interface
interface Logger {
log(message: string): void
}

// Implement interface
@TpService()
class ConsoleLogger implements Logger {
log(message: string) {
console.log(message)
}
}

// Use interface as token
platform.import({
provide: Logger,
useClass: ConsoleLogger
})

Next Steps