@perasite/tdi

0.2.8 • Public • Published

🎯 tdi

npm package minimized gzipped size GitHub Actions Workflow Status Codecov NPM Version License: MIT

🚀 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 container
  • upsertContainer(): Override existing dependencies from another container
  • addTokens(): Import specific dependencies
  • upsertTokens(): 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

📝 License

MIT © PeraSite

Package Sidebar

Install

npm i @perasite/tdi

Weekly Downloads

682

Version

0.2.8

License

MIT

Unpacked Size

55.2 kB

Total Files

9

Last publish

Collaborators

  • perasite