@perasite/tdi
v0.2.8
Published
A 1kb, zero-dependencies, immutable, type-safe IoC container for TypeScript
Downloads
1,056
Maintainers
Readme
🎯 tdi
🚀 A tiny, zero-dependencies, immutable, type-safe IoC container for TypeScript.
🌟 Why tdi?
- 📦 Tiny: < 1KB minified + gzipped
- 🧩 Simple: No decorators, reflection, or magic strings
- 🛡️ Type-safe: Full TypeScript support with type inference and compile-time checks
- ⚡ Async Support: First-class support for async dependencies
- 🔒 Immutable: New container is created without mutating the original
📥 Installation
Choose your preferred package manager:
npm install @perasite/tdi # npm
pnpm install @perasite/tdi # pnpm
yarn add @perasite/tdi # yarn
📘 Usage Examples
1️⃣ Basic DI Container
Create a container and add dependencies with automatic type inference. Access dependencies through the items
property or get()
method.
import { createContainer } from '@perasite/tdi';
// Create a container with configuration
const container = createContainer()
.add({
config: {
apiUrl: 'https://api.example.com',
timeout: 5000
}
});
// Access dependencies with full type safety
container.items.config; // { apiUrl: string; timeout: number }
container.get('config'); // does the same
Dependencies can be values, functions, or promises. Function dependencies are evaluated when accessed.
const container = createContainer()
.add({
lazyValue: () => 'Hello, world'
asyncValue: async () => 'Hello, async world'
});
container.items.lazyValue; // 'Hello, world'
await container.items.asyncValue; // 'Hello, async world'
2️⃣ Compile-time Type Safety
The container prevents errors like duplicate dependencies and accessing non-existent values at compile time.
Use upsert()
to safely update existing values when needed.
const container = createContainer()
.add({
config: { apiUrl: 'https://api.example.com' }
});
// ❌ Error: Unsafe overwrite. Use `upsert` instead
container.add({
config: { timeout: 5000 }
});
// ❌ Error: Property `missing` does not exist
container.add((ctx) => ({
service: () => ctx.missing
}));
// ✅ Valid
container
.upsert({ config: { apiUrl: 'https://new-api.com' } })
.add((ctx) => ({
newService: () => ctx.config.apiUrl
}));
3️⃣ Dependency Resolution
Dependencies are lazily evaluated, ensuring they always reflect the current state when accessed through context.
const userContainer = createContainer()
.add({
name: 'John'
});
const greetContainer = createContainer(userContainer)
.add((ctx) => ({
greet: () => `Hello, ${ctx.name}!`
}))
.add((ctx) => ({
formal: () => `${ctx.greet} How are you?`
}));
const janeContainer = greetContainer.upsert({
name: 'Jane'
});
// greet, formal are now automatically updated
janeContainer.items.name; // 'Jane'
janeContainer.items.greet; // 'Hello, Jane!'
janeContainer.items.formal; // 'Hello, Jane! How are you?'
4️⃣ Container Operations
Compose containers using various operations to manage dependencies effectively:
addContainer()
: Import all dependencies from another containerupsertContainer()
: Override existing dependencies from another containeraddTokens()
: Import specific dependenciesupsertTokens()
: Override specific dependencies
// Base container with configuration
const baseContainer = createContainer()
.add({ config: { apiUrl: 'https://api.example.com' } });
// Add all dependencies
const extendedContainer = createContainer()
.addContainer(baseContainer)
.add({ additionalConfig: { timeout: 5000 } });
// Override existing dependencies
const updatedContainer = createContainer()
.add({ config: { apiUrl: 'https://new-api.com' } })
.upsertContainer(baseContainer)
// Import specific dependencies
const specificContainer = createContainer()
.addTokens(baseContainer, 'config')
// Override specific dependencies
const specificUpdatedContainer = createContainer()
.add({ config: { apiUrl: 'https://new-api.com' } })
.upsertTokens(baseContainer, 'config');
5️⃣ Complex scenarios with testing
Create test environments by overriding production dependencies with mocks using upsert
.
interface IUserRepository {
getUser(id: number): Promise<string>;
}
class UserRepository implements IUserRepository {
async getUser(id: number): Promise<string> {
return `User ${id} from Database`;
}
}
class UserService {
constructor(private userRepository: IUserRepository) {
}
async printName(id: number) {
console.log(await this.userRepository.getUser(id));
}
}
// Production container with real implementation
const prodContainer = createContainer()
.add({
userRepository: (): IUserRepository => new UserRepository(),
})
.add(ctx => ({
userService: new UserService(ctx.userRepository),
}));
// Test container with mock implementation
const testContainer = prodContainer
.upsert({
userRepository: (): IUserRepository => ({
getUser: async () => 'Mock User',
}),
});
await prodContainer.get('userService').printName(1); // User 1 from Database
await testContainer.get('userService').printName(1); // Mock User
💬 Support
- 📫 Create an issue for bug reports
- 💡 Start a discussion for feature requests
- 🤔 Ask questions in the discussions section
📝 License
MIT © PeraSite