Clawject: Dependency Injection with Beans
Artem Korniev
Posted on October 21, 2024
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);
}
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);
}
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>);
}
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);
}
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);
}
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!
Posted on October 21, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.