Lightweight and powerful dependency injection container for TypeScript applications
first-di is a modern, type-safe dependency injection container designed specifically for TypeScript applications. It provides a clean and intuitive API for managing dependencies with minimal configuration and zero runtime dependencies.
For the latest stable version:
npm i first-di
Install reflect-metadata package and import it in your application entry point:
npm install reflect-metadata
Enable the following compiler options in tsconfig.json:
{
"compilerOptions": {
...
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
...
}
}
The simplest approach - write classes and inject dependencies through constructors. All dependencies are automatically resolved when calling resolve().
import { resolve, override, reflection } from "first-di";
@reflection // TypeScript will generate reflection metadata
class ProdRepository { // Default implementation
public async getData (): Promise<string> {
return Promise.resolve("production");
}
}
@reflection
class MockRepository implements ProdRepository { // Mock implementation with same interface
public async getData (): Promise<string> {
return Promise.resolve("mock");
}
}
@reflection
class ProdService {
public constructor (
private readonly prodRepository: ProdRepository
) { }
public async getData (): Promise<string> {
return this.prodRepository.getData();
}
}
@reflection
class ProdStore {
public constructor (
// Inject dependency
private readonly prodService: ProdService
) {
// Other logic here
}
public async getData (): Promise<string> {
return this.prodService.getData();
}
}
if (process.env.NODE_ENV === "test") { // Override in test environment
override(ProdRepository, MockRepository);
}
const store = resolve(ProdStore); // Create instance by framework
const data = await store.getData();
if (process.env.NODE_ENV === "test") {
assert.strictEqual(data, "mock");
} else {
assert.strictEqual(data, "production");
}
For advanced scenarios, use abstract classes as contracts instead of interfaces. Abstract classes generate runtime metadata that TypeScript interfaces cannot provide.
import { resolve, override, reflection } from "first-di";
abstract class AbstractRepository { // Abstract instead of interface
public abstract getData (): Promise<string>;
}
@reflection
class ProdRepository implements AbstractRepository {
public async getData (): Promise<string> {
return Promise.resolve("production");
}
}
@reflection
class MockRepository implements AbstractRepository {
public async getData (): Promise<string> {
return Promise.resolve("mock");
}
}
abstract class AbstractService { // Abstract instead of interface
public abstract getData (): Promise<string>;
}
@reflection
class ProdService implements AbstractService {
private readonly prodRepository: AbstractRepository;
public constructor (prodRepository: AbstractRepository) {
this.prodRepository = prodRepository;
}
public async getData (): Promise<string> {
return this.prodRepository.getData();
}
}
@reflection
class ProdStore {
public constructor (
private readonly prodService: AbstractService
) {}
public async getData (): Promise<string> {
return this.prodService.getData();
}
}
override(AbstractService, ProdService);
if (process.env.NODE_ENV === "test") {
override(AbstractRepository, MockRepository);
} else {
override(AbstractRepository, ProdRepository);
}
const store = resolve(ProdStore);
const data = await store.getData();
if (process.env.NODE_ENV === "test") {
assert.strictEqual(data, "mock");
} else {
assert.strictEqual(data, "production");
}
First DI has several points for customizing dependency options:
DI.defaultOptions: AutowiredOptions. Sets global default behavior.override(fromClass, toClass, options?: AutowiredOptions). Sets behavior overridden dependency.resolve(class, options?: AutowiredOptions). Sets behaviors for resolve dependencies.Options have the following properties:
lifeTime: AutowiredLifetimes - Sets lifeTime of dependency.
SINGLETON - Create one instance for all resolvers.
PER_INSTANCE - Create one instance for one resolver instance. Also called ‘transient’ or ‘factory’ in other containers.
PER_OWNED - Create one instance for one type of resolver.
PER_ACCESS - Create new instance on each access to resolved property.
Support multiple scopes
import { DI } from "first-di";
import { ProductionService } from "../services/ProductionService";
const scopeA = new DI();
const scopeB = new DI();
const serviceScopeA = scopeA.resolve(ProductionService);
const dataA = await serviceScopeA.getData();
const serviceScopeB = scopeB.resolve(ProductionService);
const dataB = await serviceScopeB.getData();
override(fromClass, toClass, options?) - Override dependency resolution with custom implementationresolve(class, options?) - Resolve dependency with specified or default optionssingleton(class) - Resolve as singleton instanceinstance(class) - Always create new instancereset() - Clear all singletons and overrides (preserves global options)The API functions can be used to implement the Service Locator pattern:
import { singleton, instance, resolve, AutowiredLifetimes } from "first-di";
class ApiDemo {
private readonly service3: ApiService3 = resolve(ApiService3, { lifeTime: AutowiredLifetimes.PER_INSTANCE });
private readonly service4: ApiService4 = singleton(ApiService4);
private readonly service5: ApiService5 = instance(ApiService5);
}
Built on OOP and SOLID principles, every component can be extended or customized:
import { DI } from "first-di";
class MyDI extends DI {
// extended method
public getAllSingletons(): IterableIterator<object> {
return this.singletonsList.values();
}
}
Contributions are welcome! Please read our Contributing Guide and Code of Conduct before submitting pull requests.
MIT © Eugene Labutin
$ claude mcp add first-di \
-- python -m otcore.mcp_server <graph>