[TypeScript] Try DI with TSyringe
Masui Masanori
Posted on June 2, 2021
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
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
}
}
Try injection
Class
lifecycleClass.ts
export class TransientClass {
public constructor() {
console.log('Transient constructor');
}
public greet() {
console.log('Hello Transient');
}
}
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();
}
}
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();
Result
Transient constructor
Caller constructor
Caller greet
Hello Transient
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');
}
...
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');
}
}
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();
}
}
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();
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.
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,
}
callSamples.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
...
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
...
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
...
@injectable()
export class Caller3 {
public constructor(@inject('LifecycleSample') private interfaceSampleClass: LifecycleSample) {
console.log('Caller3 constructor');
}
public greet() {
console.log('Caller3 greet');
this.interfaceSampleClass.greet();
}
}
Posted on June 2, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.