[TypeScript] Try DI with TSyringe
Tags: typescript, tsyringe
Published: 2021-06-02 11:51:27

## 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.

* [GitHub - microsoft/tsyringe: Lightweight dependency injection container for JavaScript/TypeScript](https://github.com/microsoft/tsyringe)

### 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
```

#### tsconfig.json
```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
  }
}
```

## Try injection
### Class
#### lifecycleClass.ts
```ts
export class TransientClass {
    public constructor() {
        console.log('Transient constructor');
    }
    public greet() {
        console.log('Hello Transient');
    }
}
```

#### callSample.ts
```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();
    }
}
```
#### index.ts
```ts
import "reflect-metadata";
import { container } from 'tsyringe';
import { Caller } from "./src/lifeCycles/callSamples";

function start() {
    const caller = container.resolve(Caller);
    caller.greet();
}
start();
```
#### Result
```
Transient constructor
Caller constructor
Caller greet
Hello Transient
```

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

#### callSample.ts
```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');
    }
...
```

## Lifetime
TSyringe has 4 scopes.

* Transient
* Singleton
* ResolutionScoped
* ContainerScoped

#### lifecycleClass.ts
```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');
    }
}
```
#### callSample.ts
```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();
    }
}
```
#### index.ts
```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();
```

#### 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
```

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.

* [scoped](https://github.com/microsoft/tsyringe#scoped)

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
```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,
}
```
#### callSamples.ts
```ts
...
@autoInjectable()
export class Caller3 {
    public constructor(private interfaceSampleClass: LifecycleSample) {
        console.log('Caller3 constructor');
    }
    public greet() {
        console.log('Caller3 greet');
        this.interfaceSampleClass.greet();
    }
}
```
#### index.ts
```ts
...
function start() {
    const caller3 = container.resolve(Caller3);
    caller3.greet();
    process.exit(0);
}
start();
```

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"
...
```

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

#### index.ts
```ts
...
function start() {
    container.register('LifecycleSample', { useClass: InterfaceSampleCalss});
    const caller3 = container.resolve(Caller3);
    caller3.greet();
    process.exit(0);
}
start();
```

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

#### callSamples.ts
```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();
    }
}
```

* [Example with interfaces](https://github.com/microsoft/tsyringe#example-with-interfaces)