DEV Community

loading...

[TypeScript] Try DI with TSyringe

Masui Masanori
Programmer, husband, father I love C#, TypeScript, etc.
・4 min read

Intro

Last time, because I had to use single database connection instance and share with every methods, so I try Dependency Injection in this time.

  • [TypeScript][PostgreSQL]Try TypeORM

There are some DI container libraries for TypeScript.
I choosed "TSyringe" because it was easy to use for me and could controll dependencies' lifetime.

Environments

  • TypeScript ver.4.3.2
  • ts-node ver.10.0.0
  • tsyringe ver.4.5.0
  • reflect-metadata ver.0.1.13

Installation

To use "TSyringe", I just installed it.
Because I had already installed TypeORM, I didn't need update tsconfig.json.

npm install --save tsyringe
Enter fullscreen mode Exit fullscreen mode

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "lib": ["DOM", "ES5", "ES2015"],
    "sourceMap": true,
    "outDir": "./js",
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Try injection

Class

lifecycleClass.ts

export class TransientClass {
    public constructor() {
        console.log('Transient constructor');
    }
    public greet() {
        console.log('Hello Transient');
    }
}
Enter fullscreen mode Exit fullscreen mode

callSample.ts

import { autoInjectable, container, inject, injectable } from "tsyringe";
import { ContainerScopedClass, ResolutionScopedClass, SingletonClass, TransientClass } from "./lifecycleClass";

@injectable()
export class Caller {
    public constructor(@inject(TransientClass) private transientClass: TransientClass) {
        console.log('Caller constructor');
    }
    public greet() {
        console.log('Caller greet');
        this.transientClass.greet();
    }
}
Enter fullscreen mode Exit fullscreen mode

index.ts

import "reflect-metadata";
import { container } from 'tsyringe';
import { Caller } from "./src/lifeCycles/callSamples";

function start() {
    const caller = container.resolve(Caller);
    caller.greet();
}
start();
Enter fullscreen mode Exit fullscreen mode

Result

Transient constructor
Caller constructor
Caller greet
Hello Transient
Enter fullscreen mode Exit fullscreen mode

I can use "@autoinjectable()" instead of "@injectable()" and "@inject()".

callSample.ts

import { autoInjectable, container, inject, injectable } from "tsyringe";
import { ContainerScopedClass, ResolutionScopedClass, SingletonClass, TransientClass } from "./lifecycleClass";

@autoInjectable()
export class Caller {
    public constructor(private transientClass: TransientClass) {
        console.log('Caller constructor');
    }
...
Enter fullscreen mode Exit fullscreen mode

Lifetime

TSyringe has 4 scopes.

  • Transient
  • Singleton
  • ResolutionScoped
  • ContainerScoped

lifecycleClass.ts

import { Lifecycle, scoped, singleton } from "tsyringe";

export class TransientClass {
    public constructor() {
        console.log('Transient constructor');
    }
    public greet() {
        console.log('Hello Transient');
    }
}
@singleton()
export class SingletonClass {
    public constructor() {
        console.log('Singleton constructor');
    }
    public greet() {
        console.log('Hello Singleton');
    }
}
@scoped(Lifecycle.ResolutionScoped)
export class ResolutionScopedClass {
    public constructor() {
        console.log('ResolutionScoped constructor');
    }
    public greet() {
        console.log('Hello ResolutionScoped');
    }
}
@scoped(Lifecycle.ContainerScoped)
export class ContainerScopedClass {
    public constructor() {
        console.log('ContainerScoped constructor');
    }
    public greet() {
        console.log('Hello ContainerScoped');
    }
}
Enter fullscreen mode Exit fullscreen mode

callSample.ts

import { autoInjectable, container, inject, injectable } from "tsyringe";
import { ContainerScopedClass, ResolutionScopedClass, SingletonClass, TransientClass } from "./lifecycleClass";

@autoInjectable()
export class Caller1 {
    public constructor(private transientClass: TransientClass,
        private singletonClass: SingletonClass,
        private resolutionScopedClass: ResolutionScopedClass,
        private containerScopedClass: ContainerScopedClass) {
        console.log('Caller1 constructor');

    }
    public greet()
    {
        console.log('Caller1 greet');

        this.transientClass.greet();
        this.singletonClass.greet();
        this.resolutionScopedClass.greet();
        this.containerScopedClass.greet();
    }
} 
@autoInjectable()
export class Caller2 {
    public constructor(private transientClass: TransientClass,
        private resolutionClass: ResolutionScopedClass,
        private caller1: Caller1) {
        console.log('Caller2 constructor');
    }
    public greet() {
        console.log('Caller2 greet');
        this.caller1.greet();
        this.transientClass.greet();
        this.resolutionClass.greet();
    }
}
Enter fullscreen mode Exit fullscreen mode

index.ts

import "reflect-metadata";
import { container } from 'tsyringe';
import { Caller1, Caller2 } from "./src/lifeCycles/callSamples";

async function start() {
    console.log('---------------- Caller 1 ----------------');
    const caller1 = container.resolve(Caller1);
    caller1.greet();
    console.log('---------------- Caller 2 ----------------');
    const caller2 = container.resolve(Caller2);
    caller2.greet();
    process.exit(0);
}
start();
Enter fullscreen mode Exit fullscreen mode

Result

---------------- Caller 1 ----------------
Transient constructor
Singleton constructor
ResolutionScoped constructor
ContainerScoped constructor
Caller1 constructor
Caller1 greet
Hello Transient
Hello Singleton
Hello ResolutionScoped
Hello ContainerScoped
---------------- Caller 2 ----------------
Transient constructor
ResolutionScoped constructor
Caller2 constructor
Caller2 greet
Hello Transient
Hello Singleton
Hello ResolutionScoped
Hello ContainerScoped
Enter fullscreen mode Exit fullscreen mode

In this sample, I can't distinguish between "Transient" and "ResolutionScoped", and "Singleton" and "ContainerScoped".

According to the document, "ContainerScoped" doesn't share same instances with child conainers.

How about "ResolutionScoped"?

I couldn't find how to it shared the same instances.

Use interface

For example, when I inject dependencies in ASP.NET Core applications, I use interfaces.

How can I inject using interfaces instead of using classes?

Problem

Of cource, I can't just change from classes to interfaces.

lifecycleClass.ts

...
@scoped(Lifecycle.ResolutionScoped)
export class InterfaceSampleCalss implements LifecycleSample {
    public constructor() {
        console.log('InterfaceSampleCalss constructor');
    }
    public greet() {
        console.log('Hello InterfaceSampleCalss');
    }
}
export interface LifecycleSample {
    greet(): void,
}
Enter fullscreen mode Exit fullscreen mode

callSamples.ts

...
@autoInjectable()
export class Caller3 {
    public constructor(private interfaceSampleClass: LifecycleSample) {
        console.log('Caller3 constructor');
    }
    public greet() {
        console.log('Caller3 greet');
        this.interfaceSampleClass.greet();
    }
}
Enter fullscreen mode Exit fullscreen mode

index.ts

...
function start() {
    const caller3 = container.resolve(Caller3);
    caller3.greet();
    process.exit(0);
}
start();
Enter fullscreen mode Exit fullscreen mode

I get a runntime error.

C:\Users\example\OneDrive\Documents\workspace\node-typeorm-sample\node_modules\tsyringe\dist\cjs\decorators\auto-injectable.js:12
                super(...args.concat(paramInfo.slice(args.length).map((type, index) => {
                                                                  ^
Error: Cannot inject the dependency at position #0 of "Caller3" constructor. Reason:
    TypeInfo not known for "Object"
...
Enter fullscreen mode Exit fullscreen mode

Resolve

I have to register the concrete class first, and resolve dependencies.

index.ts

...
function start() {
    container.register('LifecycleSample', { useClass: InterfaceSampleCalss});
    const caller3 = container.resolve(Caller3);
    caller3.greet();
    process.exit(0);
}
start();
Enter fullscreen mode Exit fullscreen mode

Another important thing is I can't use "@autoInjectable()" for this purpose.

callSamples.ts

...
@injectable()
export class Caller3 {
    public constructor(@inject('LifecycleSample') private interfaceSampleClass: LifecycleSample) {
        console.log('Caller3 constructor');
    }
    public greet() {
        console.log('Caller3 greet');
        this.interfaceSampleClass.greet();
    }
}
Enter fullscreen mode Exit fullscreen mode

Discussion (0)