tsyringe - specify which arguments to inject

789 Views Asked by At

I am using tsyringe to manage my dependency injection. The following code fragment works.

import 'reflect-metadata'
import {autoInjectable,singleton} from 'tsyringe'

interface sheepProps {
    legCount: number
}

@singleton()
export class Animal {
  makeSound (sound: string) {
    console.log(sound)
  }
}


@autoInjectable()
class Sheep {

  constructor(sheepProps?: sheepProps, private animal?:Animal) {}

  beeeeh () {
    this.animal.makeSound('bleeeeh')
  }
}

const props: sheepProps = {
    legCount: 4
} 

const sheep = new Sheep(props);
sheep.beeeeh();

However when sheepProps is not provided in the call to the constructor by changing new Sheep(props) to new Sheep(), an error is thrown:

Error: Cannot inject the dependency "sheepProps" at position #0 of "Sheep" constructor. Reason: TypeInfo not known for "Object"

To me is seems like tsyringe is trying to inject something when sheepProps is not provided in the call to the constructor.

Is this assumption correct, and how would I fix this so I can still use optional arguments in a constructor?

2

There are 2 best solutions below

0
SomeDutchGuy On BEST ANSWER

The below code solves the problem, in a way that it is still possible to use new to instantiate a class.

Note: Please be aware that when sheepProps is not given to the constructor, the value will be an empty object in this case. It is possible to set an other default, however you cannot set it to undefined!

import 'reflect-metadata'
import {autoInjectable,singleton, inject, container} from 'tsyringe'

interface SheepProps {
    legCount: number
}

@singleton()
export class Animal {
  makeSound (sound: string) {
    console.log(sound)
  }
}

const sheepPropsToken = Symbol();
container.register(sheepPropsToken, {useValue: {} });

@autoInjectable()
class Sheep {
    private props: SheepProps

  constructor(@inject(sheepPropsToken) sheepProps?: SheepProps, private animal?:Animal) {
    console.log(sheepProps)
    this.props = sheepProps
  }

  beeeeh () {
    this.animal.makeSound('bleeeeh')
  }

  getLegCount() {
    if(this.props.legCount !== undefined) {
        console.log(this.props.legCount)
    } else {
        console.log('Not known to have legs.')
    }
  }
}

const props: SheepProps = {
    legCount: 4
} 

const sheep1 = new Sheep();
sheep1.beeeeh();
sheep1.getLegCount();

const sheep2 = new Sheep(props);
sheep2.beeeeh();
sheep2.getLegCount();

Output:

{}
bleeeeh
Not known to have legs.
{ legCount: 4 }
bleeeeh
4
2
Aluan Haddad On

Given the following

import 'reflect-metadata';
import {autoInjectable, container} from 'tsyringe';
import {Animal} from './animal';

interface sheepProps {
  legCount: number;
}

@autoInjectable()
class Sheep {
  constructor(sheepProps?: sheepProps, private animal?: Animal) {}
  beeeeh() {
    this.animal?.makeSound('bleeeeh');
  }
}

const props: sheepProps = {
  legCount: 4
};

const sheep = container.resolve(Sheep);
sheep.beeeeh();

As there is no run time value corresponding to the of the first parameter, thus the dependency injection facility provided by tsyringe has no meaningful value to correlate with the type of the first parameter. In such a case, the decorator facility will correlate the unhelpful value globalThis.Object as can be seen in transpiled output below:

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import 'reflect-metadata';
import { autoInjectable, container } from 'tsyringe';
import { Animal } from './animal';
let Sheep = class Sheep {
    animal;
    constructor(sheepProps, animal) {
        this.animal = animal;
    }
    beeeeh() {
        this.animal?.makeSound('bleeeeh');
    }
};
Sheep = __decorate([
    autoInjectable(),
    __metadata("design:paramtypes", [Object, Animal])
], Sheep);
const props = {
    legCount: 4
};
const sheep = container.resolve(Sheep);
sheep.beeeeh();

To fix this behavior, we need to use the tsyringe API, specifically container to register sheepProps for injection when a dependent is requested. Please note that the decorators like singleton() and autoInjectable() implicit register the dependencies of decorated constructs and the decorated construct itself such that the container may resolve it.

sheep-props.ts

import {container} from 'tsyringe';

export interface sheepProps {
  legCount: number;
}

export const props: sheepProps = {
  legCount: 4
};


export const sheepPropsToken = Symbol();
container.register(sheepPropsToken, {useValue: props});

The above registers, props, a value of type sheepProps with the container under the token sheepPropsToken such that it may be resolved by calling container.resolve(sheepPropsToken).

Now back to the Sheep class:

import 'reflect-metadata';
import {autoInjectable, container, inject} from 'tsyringe';

import {SheepProps, sheepPropsToken} from './sheep-props';
import {Animal} from './animal';

@autoInjectable()
class Sheep {
  constructor(
    @inject(sheepPropsToken) sheepProps?: SheepProps,
    private animal?: Animal
  ) {}
  beeeeh() {
    this.animal?.makeSound('bleeeeh');
  }
}

const sheep = container.resolve(Sheep);
sheep.beeeeh();

The key here is the use of the constructor parameter decorator to specify how the container should lookup sheepProps when resolving it to construct Sheep. Note that this isn't necessary for the second parameter because there is a value, the Animal class itself with which to automatically register and resolve values of type Animal.

Here's the relevant difference in the transpiled output:

Sheep = __decorate([
    autoInjectable(),
    __param(0, inject(sheepPropsToken)),
    __metadata("design:paramtypes", [Object, Animal])
], Sheep);
const sheep = container.resolve(Sheep);
sheep.beeeeh();

The tsyringe library's autoInjectable() decorator has additional behavior, specifically replacing a decorated class with an equivalent class having a nullary constructor. This is surprising behavior but it allows creation of objects via new to use the configured dependency resolution. In other words, new Sheep() becomes equivalent to container.resolve(Sheep). This behavior composes with other aspects of the dependency injection system tsyringe provides. Wherfor autoInjectable() and inject(), container.register, and other DI related constructs work together. Personally, I find the idea of this awkward and surprising, especially as it requires us to make required arguments optional to avoid TypeScript checker errors. It does however work.

const sheep = new Sheep;
sheep.beeeeh();