2.3.0 • Published 11 months ago

karambit-inject v2.3.0

Weekly downloads
-
License
Apache-2.0
Repository
github
Last release
11 months ago

Karambit

NPM version NPM License Build

A compile-time and type-safe dependency injector for Typescript.

Karambit is different from other Typescript dependency injection libraries in several key ways:

  • It is a fully compile-time framework. Code is generated during the compile process, and there is no additional runtime dependency.
  • There is no need to mark or annotate most parameters, including interfaces, with "tokens". Karambit supports binding interfaces to concrete types directly.
  • Karambit is fully type-safe. It is not possible to register a provider with the wrong type.^1
  • The dependency graph is fully validated during compilation, so if your project builds then you can be certain the graph is valid. This includes detecting missing bindings, dependency cycles, and other common error sources.
  • Creating a graph is fully declarative; you never need to imperatively register a dependency to a container. Instead, providers are declared idiomatically as constructors and static methods.

Karambit is heavily inspired by Dagger, and if you're familiar with that then you'll be right at home using Karambit.

^1: Of course, you can still use Typescript features like any or as to break anything you want :)

Installation

This project is available as a package in NPM.

$ npm install --save-dev karambit-inject

Karambit is a decorator-based framework; you'll currently need to enable the experimentalDecorators flag in your Typescript compiler options to use the Karambit API. However, please note that these decorators are stripped away during compilation and will not exist in the transpiled JavaScript source code. By extension, run-time reflection metadata is not needed, and you do not need to enable the emitDecoratorMetadata flag to use Karambit.

Because Karambit needs to run during the compilation process, you will also have to configure it to run as a transformer.

There are many ways to set this up, but ttypescript or ts-patch are among the simplest.

For a minimal example project, check out the Hello World sample.

Note This guide assumes you're using the official Typescript compiler (tsc) to build your code. If you're using another method of transpilation to JavaScript, your configuration will differ.

Warning Because Karambit decorated source files do not output deterministically based on their own content (i.e., other files can affect the output), these files do not play nicely with the incremental compiler. A clean build may be required when modifying code marked with a Karambit decorator.

ttypescript

$ npm install --save-dev ttypescript

Update your build scripts to use ttsc instead of tsc. For example, in your package.json:

{
  "scripts": {
    "build": "ttsc"
  }
}

Finally, add the Karambit transformer as a plugin inside your .tsconfig:

{
  "experimentalDecorators": true,
  "compilerOptions": {
    "plugins": [
      { "transform": "karambit-inject" }
    ]
  }
}

Getting started

Components

Fundamentally, Karambit works by generating an implementation of each class marked @Component.

The Component is what ultimately hosts a dependency graph, and how you expose that graph to other parts of your application. You can think of the Component as the entry-point into your graph. In the Hello World sample, the Component looks like this:

@Component({modules: [HelloWorldModule]})
abstract class HelloWorldComponent {
    abstract readonly greeter: Greeter
}

This Component exposes a single type, the Greeter, which during compile will be implemented by Karambit.

Providers

@Inject

The next step is to satisfy the dependency graph of the Component. Karambit isn't magic; you need to specify how to get an instance of each type in the graph.

There are several ways to do this, but the simplest is to mark a class with @Inject. This makes the constructor of that class available to Karambit, and Karambit will call the constructor to provide an instance of that type.

In this sample, the Greeter class is marked @Inject, and this type is available in the graph.

@Inject
class Greeter {
    constructor(private readonly greeting: string) { }
    greet(): string {
        return `${this.greeting}, World!`
    }
}

@Provides

The constructor of Greeter depends on one other type: string. However, this type doesn't have a constructor and, even if it did, we don't control the source code to mark it with @Inject. This is where Modules come in to play.

A module is a collection of static methods marked with @Provides and each Component can install many Modules. These provider methods work just like @Inject constructors; they can have arguments and will be used by Karambit to provide an instance of their return type.

In our example, the string type is provided in the HelloWorldModule:

@Module
abstract class HelloWorldModule {

    @Provides
    static provideGreeting(): string {
        return "Hello"
    }
}

You can think of Modules as the private implementation of a Component, which itself is sort of a public interface.

Modules are installed in Components via the modules parameter of the @Component decorator.

@Component({modules: [HelloWorldModule]})

Putting it all together

By providing the string type into our graph, all the required types are now satisfied with providers and Karambit can generate our Component implementation.

You can instantiate a Component by calling createComponent, and access its properties just like any other class instance:

const component: HelloWorldComponent = createComponent<typeof HelloWorldComponent>()
console.log(component.greeter.greet()) // "Hello, World!"

Under the hood, Karambit has generated this implementation in the output JavaScript source:

class KarambitHelloWorldComponent extends HelloWorldComponent {
    get greeter() { return this.getGreeter_1(); }
    getGreeter_1() { return new Greeter(this.getString_1()); }
    getString_1() { return HelloWorldModule.provideGreeting(); }
}

While this example is clearly a bit contrived, you should be able to see how simple it can be to add new types to a graph and build much more complex dependency structures.

This is only scratching the surface of what Karambit is capable of, so check out the feature guide for a more in-depth look at everything it has to offer. For a small, real-world migration example, check out this PR that bootstrapped Karambit to use itself for dependency injection.

License

Copyright 2022-2023 Devin Price

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
2.3.0

11 months ago

2.2.0

1 year ago

2.1.1

1 year ago

2.1.0

1 year ago

1.2.0

1 year ago

1.1.1

1 year ago

1.1.0

2 years ago

1.0.8

2 years ago

1.1.2

1 year ago

2.0.0

1 year ago

1.0.7

2 years ago

1.0.6

2 years ago

1.0.5

2 years ago

1.0.4

2 years ago

1.0.3

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago

0.1.1

2 years ago

0.1.0

2 years ago

0.1.0-alpha

2 years ago