Giter Club home page Giter Club logo

ioc-check-ts's Introduction

IOC-Check

GitHub Workflow Status License NPM Sponsors

Runtime checks to catch dependency injection configuration errors.

Table of contents

License & Support

This package is licensed under the MIT license. Please consider supporting me and my projects by becoming a sponsor.

Motivation

Dependency injection or inversion of control frameworks are awesome 99% of the time. They are not so awesome when your tests fail in unexpected ways, or when your production build contains a class that belongs to a unit test.

My workmates and I therefore consider it a best practice to check the injected values during runtime. If they match an abstract class, which we often use as our implementation interfaces, we let the app crash. This has allowed us to catch bugs in the past and will make developing unit tests and predict their failures easier in the future.

Writing these runtime checks is however laborsome, error prone and repetitive. I therefore generalized the problem and here we are.

Usage

Currently, there are two flavours of injection checks:

  • throwIfMatches will throw an error if an instance of a class matches a class that is known to be incorrect one. Use this check whenever you need to be certain that you injected the correct class.
  • noDirectInstantiation is a class decorator that adds a runtime check against direct instantiation. The class becomes an abstract class during runtime.

The usage of these approaches can be mixed. My suggestion would be to use noDirectInstantiation in conjunction with good documentation about its behaviour. The usage of abstract classes in your codebase might also help prevent unexpected behaviour. throwIfMatches is the more verbose and configurable variant of the two.

throwIfMatches

The general usage is as follows:

class A {}

class B extends A {}

const {throwIfMatches} = require("ioc-check/throwIfMatches");

const instance1 = new B(); // or create an instance any other way
const instance2 = new A(); // e.g. ioc and dep-inj

throwIfMatches(instance1, A); // ok
throwIfMatches(instance2, A); // throws DependencyInjectionError

Create instances of objects however you like, in this case by calling the constructor directly. In practice, you would probably want to use a inversion of control framework or implement dependency injection yourself.
Then, compare the instance of your object against a class that you know your instance should not be an instance of. In this specific example the comparison of the instance of B against A will pass the check, since the classes do not match exactly. However, the comparison of the instance of A against A will not pass the check, since A -and only A- matches A exactly. Thus, a DependencyInjectionError will be thrown.

An example that is perhaps more realistic is as follows:

import {throwIfMatches} from "ioc-check/throwIfMatches";
import {Inject} from "typescript-ioc";

abstract class Fruit {
    abstract takeABite();
}

class Apple extends Fruit {
    takeABite() {
        console.log("yummy ๐Ÿ‘Œ nomnom ๐Ÿ˜Š");
    }
}

class Human {
    private readonly food: Fruit;

    constructor(@Inject somethingToEat: Fruit) {
        throwIfMatches(somethingToEat, Fruit);
        this.food = somethingToEat;
    }
}

In this example we use ioc-check to check if the dependency injection by the popular typescript-ioc worked as expected. Depending on how our ioc is configured, we could bind Apple to Fruit or leave Fruit unbound. The latter could for example be the case in unit tests, or when developing new components for the existing application. Explicit checks for the correct configuration are oftentimes required to catch oversights. In our example above, we would not want to let Fruit be unbound, since we apparently need instances of the class to do something in Human. Thus the check is introduced, if Fruit is bound to a subclass, or a different class that is similar enough for typescript type checks to pass.

Depending on how Fruit is bound, we can now observe different behaviours during runtime.

Container.bind(Fruit).to(Apple);
const steve = Container.get(Human);

In this case steve will be successfully instantiated as the ioc runtime check has passed. His food property will be populated with a new Apple object.

Container.bind(Fruit).to(Fruit); // or simply omit this line
const steve = Container.get(Human); // throws DependencyInjectionError reason Fruit

In this case steve will not be instantiated and a DependencyInjectionError will be thrown. His food property would have been populated with a new Fruit object. In this example an instantiation with a Fruit would have been nonsensical, since Fruit should only be instantiated through subclasses. Thankfully, our manual check with throwIfMatches caught this potential bug.

One downside of this approach to dependency injection checks is that you have to manually test for the correct or incorrect class. There is no built-in guarantee that you did not forget to check your classes. To save yourself some time and the headache of manual test, check out noDirectInstantiation.

noDirectInstantiation

Instead of adding these checks everywhere you need them, why not simply add the check to the class itself? This is exactly what the class decorator function noDirectInstantiation does. It essentially makes a class abstract during runtime.

The usage is pretty simple and much less verbose than the usage of throwIfMatches once you figure out how to enable decorator functions for your use case. If you are using TypeScript, it is as easy as setting the configuration parameter enableExperimentalDecorators to true.

Let's revisit the example from before but slightly modified:

import {noDirectInstantiation} from "ioc-check/noDirectInstantiation";

@noDirectInstantiation // add this decorator to prevent direct instantiation
abstract class Fruit {
    abstract takeABite();
}

class Apple extends Fruit {
    takeABite() {
       console.log("yummy ๐Ÿ‘Œ nomnom ๐Ÿ˜Š"); 
    }
}

new Apple(); // this is fine
// @ts-expect-error TypeScript does not like it when you instantiate an abstract class
new Fruit(); // throws DependencyInjectionError reason Fruit

Fruit cannot be directly instantiated after applying the decorator, only by extending it and then instantiating the subclass. Just like throwIfMatches, this is a very useful sanity check during runtime, especially in conjunction with typescript-ioc or other dependency injection frameworks. Since they do not know about abstract classes during runtime - this piece of information is lost after compiling - they will happily create instances of your abstract classes. With the addition of the decorator, they will however not be able to do so anymore.

import {Inject} from "typescript-ioc";

class Human {
    private readonly food: Fruit;

    constructor(@Inject somethingToEat: Fruit) {
        this.food = somethingToEat;
    }
}

Depending on how Fruit is bound, its instantiation through typescript-ioc will now produce a runtime error, just like in the example of throwIfMatches. If Fruit is instantiated directly, it will now produce a runtime Error:

DependencyInjectionError: Fruit
  at [...]

The advantage of this approach is also it's biggest downside. While you only have to configure this behaviour once and not explicitly check for the instances class type before usage, this behaviour is also less apparent and slightly obscured. It is also less flexible, since you might have cases where you can ignore the correct or incorrect instantiation. For a more verbose, configurable but less robus approach, check out throwIfMatches.

Installation

npm i --save ioc-check

The npm package contains the transpiled JavaScript code and TypeScript typings. Both are generated from the TypeScript source code. You do not need TypeScript to use this package.

Both flavors of decorators in TypeScript are supported: experimental and TC39 proposal, available from typescript@^5.

Updates

ioc-check follows Semantic Versioning 2.0.0. This means that you can decide based on the version number of the package if manual update intervention is required. Head over to Github Releases or check the CHANGELOG file for changes between versions. In most cases npm will take care of updates for you automatically with npm update or an alternative of your choice.

(Not recommended) npm i --save ioc-check@latest will force the package to the latest version in any case.

ioc-check-ts's People

Contributors

dependabot[bot] avatar miladiir avatar

Watchers

 avatar

ioc-check-ts's Issues

noDirectInstantiation parameter and return type

Currently the parameter and return type for the constructor decorator noDirectInstantiation is any, because I couldn't figure out how to type it correctly.

Maybe there is a type that expresses what we are trying to do precisely. The input type is of course a class constructor of a class that can be either abstract or instantiable. The output type is then similar.

noDirectInstantiation fails

Describe the bug
Using @noDirectInstantiation fails with error:
"Can not identify the base Type for requested target..."

To Reproduce
Use the @noDirectInstantiation decorator with any abstract class and NodeJS 16.13.0

Expected behavior
Should not throw runtime error

Greetings from YS

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.