Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

true-di

Package Overview
Dependencies
Maintainers
1
Versions
21
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

true-di

Zero Dependency, Minimalistic **Type-Safe DI Container** for TypeScript and JavaScript projects

  • 4.0.0
  • Source
  • npm
  • Socket score

Version published
Maintainers
1
Created
Source

true-di

Zero Dependency, Minimalistic Type-Safe DI Container for TypeScript and JavaScript projects

Build Status Coverage Status npm version npm downloads GitHub license

Installation

npm i --save true-di
yarn add true-di

Usage Example

The following code is an example solution that is based on the example used in the book "Dependency Injection Principles, Practices, and Patterns" by Steven van Deursen and Mark Seemann.

The problem is to display the list of featured products and discount their prices for the preferred customers.

More details on the example could be found in the "Getting Started" Example Project README file.

./src/main.ts - composition root

import Module from 'true-di';
import { DiscountService } from './DiscountService';
import { Product } from './domain/products';
import { ProductRepoMock } from './ProductRepoMock';
import { ProductService } from './ProductService';
import { UserService } from './UserService';
import PRODUCTS_JSON from './products.json';

const main = Module()
  .private({
    productRepo: () =>
      new ProductRepoMock(PRODUCTS_JSON as Product[]),
  })
  .public({
    userService: (_, { token }: { token: string | null }) =>
      new UserService(token),
  })
  .private({
    discountService: ({ userService }) =>
      new DiscountService(userService),
  })
  .public({
    productService: ({ productRepo, discountService }) =>
      new ProductService(productRepo, discountService),
  });

export default main;

./src/DiscountService/index.ts

import { IDiscountService, IUserProvider } from '../interfaces';

export const PREFERRED_CUSTOMER_DISCOUNT: number = 0.05;
export const NO_DISCOUNT: number = 0;

export class DiscountService implements IDiscountService {
  constructor(
    private readonly userService: IUserProvider,
  ) {}

  async getDiscountRate() {
    const user = await this.userService.getCurrentUser();
    return user != null && user.isPreferredCustomer
      ? PREFERRED_CUSTOMER_DISCOUNT
      : NO_DISCOUNT;
  }
}

./src/ProductRepoMock/index.ts

import { Product } from '../domain/products';
import { IProductRepo } from '../interfaces';
import { matches } from '../utils/matches';

export class ProductRepoMock implements IProductRepo {
  constructor(private readonly products: Product[]) {}

  async getProducts(match?: Partial<Product>): Promise<Product[]> {
    return match != null ? this.products.filter(matches(match)) : this.products.slice();
  }
}

./src/UserService/index.ts

import { parseToken } from '../domain/user-token';
import { User } from '../domain/users';
import { IUserService } from '../interfaces';

export class UserService implements IUserService {
  #user: User | null = null;

  #token: string | null = null;

  constructor(readonly token: string | null) {
    this.#token = token;
  }

  async getCurrentUser(): Promise<User | null> {
    if (this.#user == null && this.#token != null) {
      this.#user = parseToken(this.#token);
    }

    return this.#user;
  }
}

./src/ProductService/index.ts

import { applyDiscount } from '../domain/products';
import {
  IDiscountRateProvider,
  IProductService,
  IProductsProvider,
} from '../interfaces';

export class ProductService implements IProductService {
  constructor(
    private readonly products: IProductsProvider,
    private readonly discountService: IDiscountRateProvider,
  ) {}

  async getFeaturedProducts() {
    const discountRate = await this.discountService.getDiscountRate();
    const featuredProducts = await this.products.getProducts({ isFeatured: true });

    return discountRate > 0
      ? featuredProducts.map(applyDiscount(discountRate))
      : featuredProducts;
  }
}

./src/products-controller.ts

import type { Request, Response } from 'express';
import { JSONMoneyReplacer } from './domain/money';
import { IFeaturedProductProvider } from './interfaces/IProductService';

type GetFeaturedProductsDeps = {
  productService: IFeaturedProductProvider;
}

export const getFeaturedProducts =
  (req: Request, res: Response) =>
    async ({ productService }: GetFeaturedProductsDeps) => {
      const featuredProducts = await productService.getFeaturedProducts();

      res
        .status(200)
        .type('application/json')
        .send(JSON.stringify(featuredProducts, JSONMoneyReplacer, 2));
    };

./src/index.ts

import express from 'express';
import createContext from 'express-async-context';
import main from './main';
import { getFeaturedProducts } from './products-controller';

const app = express();

const Context = createContext(
  req => main.create({ token: req.headers.authorization ?? null }),
);

app.use(Context.provider);

app.get('/featured-products', Context.consumer(getFeaturedProducts));

if (module === require.main) {
  app.listen(8080, () => {
    console.log('Server is listening on port: 8080');
    console.log('Follow: http://localhost:8080/featured-products');

    console.log(`
    . .env && curl -H "authorization: \${USER_TOKEN}" http://localhost:8080/featured-products
    `);
  });
}

export default app;

Some Concepts

Module

In the context of true-di, a Module is a collection of named and typed items that can communicate with each other. These items can be either private,meaning they are only accessible within the Module, or public, which allows them to be accessed both inside and outside of the Module.

Essentially, a Module can be thought of as an instance of a class, with private and public fields. However, in true-di, none of the items within the Module are created until they are requested from outside of the module, including the function provided to the .expose method of the ModuleBuilder.

The Module could be created by invoking the Module Factory function.

Module Factory

The Module Factory is a function that takes an object called external dependencies as input and uses it to create a Module. Later on, when an item within the Module is requested, the corresponding Resolver will receive the external dependencies as input to resolve the item.

In true-di, the Module Factory is essentially a composition of Resolvers that have been defined for each item within the module. This means that the code defining the Module Factory serves as the Composition Root, where all loosely coupled components such as classes and functions are brought together and assembled in a cohesive manner.

Resolver & Resolution Rule

The Resolver is an item factory, that receives two parameters:

  • internal dependencies - an object containing all Module items (excluding the creating one)
  • external dependencies - an object passed to the Module Factory

Resolver is an implementation of the Resolution Rule, the rule defines how to create a specific item using other items of Module and external dependencies.

Unlike some other dependency injection solutions, true-di does not rely on interfaces or type names to define how to create an item. Instead, it uses explicit item names and Resolvers defined for each name. This approach avoids the need for additional abstractions in cases where a single interface is implemented by several different components (classes).

Also it allows to use true-di with JS-projects those don't have any interfaces or type names nor in build time, nor in runtime.

Initializers

Initializer - is a function associated with a some item of Module that receives two arguments:

  • the item to be initialized
  • the internal dependencies object with all items of the Module

And initializes the item.

The Initializer is an implement of the "Injection via property" pattern that is useful to resolve cyclic dependencies.

Note: The Cyclic Dependencies is an anti-pattern and should be avoided whenever it is possible.

The Initializers is an object that maps item names to correspondent item Initializer

Module Builder

Module Use Cases

Life Time Scope

API Reference

Keywords

FAQs

Package last updated on 25 Mar 2023

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc