tsyringe

repository·master·Indexed 26 days ago

https://github.com/microsoft/tsyringe

A lightweight dependency injection container for TypeScript and JavaScript specializing in constructor injection. It provides decorators such as @injectable, @singleton, and @scoped to manage object lifecycles and dependencies, supporting class, value, factory, and token providers. Features include child container creation, circular dependency resolution via delay(), and integration with reflect-metadata.

Tokens
7.7K
Snippets
25
Records
71
Agent score
88%

What's inside tsyringe

  1. Register Providers in the Container

    master

    To use decorated classes, they must be registered with the container using container.register(). You can use several provider types to define how a token is resolved:

    • Class Provider: Resolves a token using a class constructor (useClass).
    • Value Provider: Resolves a token to a specific, pre-existing value (useValue).
    • Factory Provider: Resolves a token using a factory function (useFactory) that has access to the DependencyContainer.
    • Token Provider: Acts as an alias, redirecting one token to another (useToken).
    container.register<Foo>(Foo, {useClass: Foo});
    container.register<Bar>(Bar, {useValue: new Bar()});
    container.register<Baz>("MyBaz", {useValue: new Baz()});
  2. Configure TypeScript for TSyringe

    master

    To use decorators and metadata required by TSyringe, update your tsconfig.json to include experimentalDecorators and emitDecoratorMetadata set to true.

    {
      "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
      }
    }
  3. Setup Reflect Metadata polyfill

    master

    TSyringe requires a Reflect API polyfill. You must import the polyfill (e.g., reflect-metadata) exactly once, and it must be imported before any dependency injection is used.

    // main.ts
    import "reflect-metadata";
    
    // Your code here...
  4. Create child containers

    master

    You can create child containers using container.createChildContainer().

    Child containers have their own independent registrations. However, if a token is not found in the child container, the container will attempt to resolve it from the parent container. This is ideal for creating per-request containers that share common services from a root container.

    const childContainer1 = container.createChildContainer();
    const childContainer2 = container.createChildContainer();
    const grandChildContainer = childContainer1.createChildContainer();
  5. Resolve circular dependencies using `delay()`

    master

    When two or more services have cyclic dependencies, container.resolve() will fail because one constructor will be undefined during the construction of the other. To resolve this, use the delay() helper function inside the @inject() decorator.

    delay() wraps the constructor in a DelayedConstructor, which acts as a special InjectionToken. It creates a proxy object that is evaluated only when used for the first time, allowing the circular dependency to be resolved transparently.

    @injectable()
    export class Foo {
      constructor(@inject(delay(() => Bar)) public bar: Bar) {}
    }
    
    @injectable()
    export class Bar {
      constructor(@inject(delay(() => Foo)) public foo: Foo) {}
    }
    
    // construction of foo is possible
    const foo = container.resolve(Foo);
    
    // property bar will hold a proxy that looks and acts as a real Bar instance.
    foo.bar instanceof Bar; // true
  6. Configure Babel for TSyringe

    master

    If using Babel (e.g., React Native), you must configure it to emit TypeScript metadata using babel-plugin-transform-typescript-metadata.

    1. Install the plugin:

      • yarn: yarn add --dev babel-plugin-transform-typescript-metadata
      • npm: npm install --save-dev babel-plugin-transform-typescript-metadata
    2. Add it to your Babel configuration plugins array.

    plugins: [
      'babel-plugin-transform-typescript-metadata',
      /* ...the rest of your config... */
    ]
  7. Inject services using interfaces and named tokens

    master

    Because interfaces do not exist at runtime, you must use the @inject() decorator with a unique string token to tell the container how to resolve the dependency. You must also register the implementation class to that token using container.register().

    // SuperService.ts
    export interface SuperService {}
    
    // TestService.ts
    import {SuperService} from "./SuperService";
    export class TestService implements SuperService {}
    
    // Client.ts
    import {injectable, inject} from "tsyringe";
    
    @injectable()
    export class Client {
      constructor(@inject("SuperService") private service: SuperService) {}
    }
    
    // main.ts
    import "reflect-metadata";
    import {container} from "tsyringe";
    import {Client} from "./Client";
    import {TestService} from "./TestService";
    
    container.register("SuperService", {
      useClass: TestService
    });
    
    const client = container.resolve(Client);
  8. Handle circular dependencies with interfaces and `delay()`

    master

    If you are using interfaces to manage circular dependencies, you can use delay() within the @registry() decorator's useToken option. This allows the DelayedConstructor to be used as the token for the interface injection.

    export interface IFoo {}
    
    @injectable()
    @registry([
      {
        token: "IBar",
        // `DelayedConstructor` of Bar will be the token
        useToken: delay(() => Bar)
      }
    ])
    export class Foo implements IFoo {
      constructor(@inject("IBar") public bar: IBar) {}
    }
    
    export interface IBar {}
    
    @injectable()
    @registry([
      {
        token: "IFoo",
        useToken: delay(() => Foo)
      }
    ])
    export class Bar implements IBar {
      constructor(@inject("IFoo") public foo: IFoo) {}
    }
  9. Resolve classes without interfaces

    master

    Since classes retain type information at runtime, you can resolve them directly without extra configuration or tokens.

    // Foo.ts
    export class Foo {}
    
    // Bar.ts
    import {Foo} from "./Foo";
    import {injectable} from "tsyringe";
    
    @injectable()
    export class Bar {
      constructor(public myFoo: Foo) {}
    }
    
    // main.ts
    import "reflect-metadata";
    import {container} from "tsyringe";
    import {Bar} from "./Bar";
    
    const myBar = container.resolve(Bar);
    // myBar.myFoo => An instance of Foo
  10. Inject primitive values using named injection

    master

    To inject primitive values (like strings, numbers, or booleans), use named injection. Register the value in the container using a unique string token and the useValue option, then use @inject(TOKEN) in the constructor.

    import {singleton, inject} from "tsyringe";
    
    @singleton()
    class Foo {
      private str: string;
      constructor(@inject("SpecialString") value: string) {
        this.str = value;
      }
    }
    
    // some other file
    import "reflect-metadata";
    import {container} from "tsyringe";
    import {Foo} from "./foo";
    
    const str = "test";
    container.register("SpecialString", {useValue: str});
    
    const instance = container.resolve(Foo);
  11. Intercept resolution with beforeResolution and afterResolution

    master

    Interception allows you to execute callbacks during the resolution lifecycle.

    • beforeResolution(token, callback, options): Executes before an object is resolved. Use this for initialization or logging.
    • afterResolution(token, callback, options): Executes after an object is resolved. Use this to call initialization methods on the resulting instance.

    Both accept an options object with a frequency property (e.g., "Always" or "Once").

    // Before resolution
    container.beforeResolution(
      Bar,
      () => {
        console.log("Bar is about to be resolved!");
      },
      {frequency: "Always"}
    );
    
    // After resolution
    container.afterResolution(
      Bar,
      (_t, result) => {
        result.init();
      },
      {frequency: "Once"}
    );