DEV Community

Cover image for Clawject: Dependency Injection with Beans
Artem Korniev
Artem Korniev

Posted on • Edited on

Clawject: Dependency Injection with Beans

Cover photo by Jay Mantri on Unsplash

Outline

Greetings everyone! I am the creator of Clawject, dependency injection (DI) framework for TypeScript. If you've ever worked with complex applications written in OOP style, you know how hard it is to manage constructor dependencies across classes. Today, I want to share with you how Clawject solves complexities of dependency injection pattern.

Visit clawject.com to find docs.

What Is a Bean?

In Clawject, a Bean is an object that is managed and created by the Clawject container but defined by you. It can have its own dependencies and simultaneously be a dependency for other Beans. Think of it like gears in a machine working together to ensure the smooth operation of the entire application. Beans give you flexibility to assemble classes with just a few lines of code and guarantee type safety.

Clawject container

Defining a DI container with Clawject is really simple, you just need to add the @ClawjectApplication decorator on top of any class, and it will automatically become a DI container.

Read more about IoC container here

@Bean — Your Best Friend

With the @Bean decorator, you can turn any property, getter, or method into a Bean.

Try this example on StackBlitz.

interface Foo {
  name: string;
}

class FooImpl implements Foo {
  name = 'foo implementation name';
}

class Bar {
  constructor(foo: Foo) {
    console.log('Bar is created, fooName: ', foo.name);
  }

  /*...*/
}

@ClawjectApplication
class Application {
  @Bean
  foo = () => new FooImpl();

  @Bean
  bar = (foo: Foo) => new Bar(foo);
}
Enter fullscreen mode Exit fullscreen mode

In this example, we declared a factory-method Bean foo that returns a new instance of the FooImpl class, and a Bean bar that depends on foo and console.logs its name. By declaring foo: Foo as a parameter of factory-function, we're telling the container that bar needs instance of a foo interface to be injected.
Clawject will take advantage of typescript's type system, will qualify that class FooImpl is an implementation of the Foo interface, and will inject instance into the bar bean as a parameter.

Tired of Extra Code? Use the Bean Function

If you have a lot of classes with multiple dependencies, listing them all can be tedious, especially when it comes to adding new constructor parameters, reordering, or removing them. The Bean function comes to the rescue by automatically creating an instance of the class with all the necessary dependencies. You can use @Bean decorator and Bean function together.

Try this example on StackBlitz.

interface Foo {}
interface Bar {}
interface Baz {}

class BarImpl implements Bar {/*...*/}
class BazImpl implements Baz {/*...*/}
class FooImpl implements Foo {
  constructor(
    dependency0: string,
    dependency1: number,
    dependency2: Bar,
    dependency3: Baz
  ) {}
  /*...*/
}

@ClawjectApplication
class Application {
  foo = Bean(FooImpl);

  @Bean
  stringBean = 'dependency0';
  @Bean
  numberBean = 1;
  barBean = Bean(BarImpl);
  bazBean = Bean(BazImpl);
}
Enter fullscreen mode Exit fullscreen mode

Bean Types — Freedom of Choice

Clawject allows you to specify Beans with almost any type. You can explicitly specify the type or let TypeScript infer it for you. The main thing is to avoid some restricted types like undefined, null, or never.

Generic types

Generic types are a really powerful language feature that unleashes flexibility and reusability of code, but in traditional DI frameworks it's really painful to work with generics in types/interfaces/classes, because they require you to provide an injection token to use them; otherwise, the JS runtime will not know what to inject. Using injection tokens is not only tedious but also dangerous, the injection tokens could be mixed up, leading to the wrong object being injected. It's a nightmare.

Clawject, on the contrary, takes full advantage of typescripts powerful type system and allows you to harness their power without fear.

Let's take a look at how we can utilize generics with the typeorm package.

import { DataSource, Entity, Repository } from 'typeorm';

@Entity()
class User {/*...*/}
@Entity()
class Post {/*...*/}

class Service<T> {
  constructor(repository: Repository<T>) {}
}

@ClawjectApplication
class Application {
  @Bean dataSource = new DataSource(/*...*/).initialize();
  @Bean usersRepository = (ds: DataSource) => ds.getRepository(User);
  @Bean postsRepository = (ds: DataSource) => ds.getRepository(Post);

  userService = Bean(Service<User>);
  postService = Bean(Service<Post>);
}
Enter fullscreen mode Exit fullscreen mode

We've defined dataSource alongside with usersRepository and postsRepository beans, then we've registered Service<User> and Service<Post> as a beans. Clawject will resolve all constructor parameter types together with generics and will inject them without any additional effort from your side!
Also, since Clawject utilises typescript's type system, you can use any third-party libraries to define your beans, all types will be resolved by Clawject.

Implementation and Inheritance types

TypeScript classes and interfaces have awesome features like inheritance and implementation:

Try this example on StackBlitz.

interface Foo {
  foo: string;
}

interface Bar extends Foo {
  bar: string;
}

class FooBarImpl implements Bar {
  foo = 'fooValue';
  bar = 'BarValue';
}

class Baz extends FooBarImpl { /*...*/ }

class Fizz {
  constructor(
    foo: Foo,
    bar: Bar,
    fooBarImpl: FooBarImpl,
    baz: Baz
  ) { /*...*/ }
}

@ClawjectApplication
class Application {
  fooBarBaz = Bean(Baz);
  fizz = Bean(Fizz);
}
Enter fullscreen mode Exit fullscreen mode

Clawject will qualify that Baz class extends FooBarImpl which is implements Bar which is extends Foo, so the same instance will be injected in fizz bean. It works great with generics as well.

Async Beans - Flexible way of configuring beans

Sometimes you need to fetch data and use it as a dependency in your constructor. It may be a secret or just a config value. In Clawject, you can easily create asynchronous Beans by simply assigning Promise to a Bean.

Try this example on StackBlitz.

interface Config {
  bar: string;
  baz: string;
}

class Foo {
  constructor(config: Config) { /*...*/ }
}

@ClawjectApplication
class Application {
  @Bean async config(): Promise<Config> { /*...*/ }

  foo = Bean(Foo);
}
Enter fullscreen mode Exit fullscreen mode

Clawject will wait for asynchronous beans and only then create dependent beans.

Note that in class Foo you're just defining dependency type as Config and not Promise<Config>.

Conclusion

Clawject and its DI system are designed to make your life as a developer simpler and more enjoyable. Clawject strives to provide tools that not only speed up the development process but also make your code more reliable and resilient to changes.

Try Clawject in action and see for yourself how powerful and flexible this dependency management tool is for TypeScript.


Happy coding!

Top comments (0)