Skip to content
Andreas Ernst edited this page Mar 5, 2026 · 10 revisions

Library scope

The core library provides basic mechanisms that are used by the other packages:

  • dependency injection
  • aop
  • configuration values
  • tracing

Dependency Injection

Dependency is a pattern especially used in large scale applications, in order to increase flexibility, testability and last but mot least the quality and maintainability, by letting a container assemble the different parts of an application - instead or having to write hardwired dependencies - and also taking care of the lifecycle. In my mind a must for every non-trivial application, especially since the coding and runtime overhead is minimal.

Registration

In contrast to frameworks that rely on manual registration, this approach relies completely on annotated classes. Let's look at the different possibilities:

Class

Every class annotated with @injectable is eligible for injection.

Example:

@injectable()
class Bar {
  const Bar();
}

Possible arguments of the annotation are:

  • scope? string defines the lifecycle of the instance. Supported scopes are "singleton", "request", "environment". The default is singleton
  • 'eager?: boolean' if false - or missing - , the instance will be created on demand, if true directly during the initialization of the containers.
  • 'module?: string` determines the mapping to modules.

We will come back to the scopes later.

The constructor can define parameters, as long as they are injectable as well.

@injectable()
class Foo {
  const Foo(private bar: Bar);
}

In this case, the container will construct the objects in the correct order, and call the constructors with the appropriate arguments. The container will of course detect cycles, in which case an exception is thrown.

Factory Methods

Every injectable class can define additional methods, that act as factories for additional instances, by annotating methods with @create().

@injectable()
class Factory {
  @create()
  createBar() : Bar {
    return new Bar();
  }
}

The methods can contain any number of injectable arguments. The @create() accept the same arguments as @injectable.

Lifecycle Methods

It is possible to annotate methods with specific annotations to mark them as callbacks which will be called during construction of the instance. The annotations relate to the lifecycle phases which are:

  • @inject: methods called after constructor instantiation, that provide additional attributes for construction
  • @onInit: called after the inject phase, assuming that the object is now completely initialized
  • @onRunning: called after all objects have been constructed
  • @onDestroy: called after the container has been destroyed

In case of parent classes, all methods except @onDestroy are called from parent to child classes!

All methods can declare any number of injectable parameters.

Example:

@injectable()
class Foo{
  const Foo();

  @onInit()
  onInit(environment: Environment) {
    ...
  }

  @onRunning()
  void start(manager: ConfigurationManger) {
    ...
  }

  @onDestroy()
  void stop() {
    ...
  }
}

Parameters

As already mentioned, a couple of methods - including the constructor - can declare any number parameters that are injectable. These are:

  • injectable classes
  • the type Environment
  • annotated parameters that can be resolved.

Currently the annotation @value(key: string, defaultValue?: any) is possible to resolve configuration values. Check the corresponding chapter.

Post Processors

Injectable classes my extends the base class PostProcessor giving them the chance to post process any newly cerated instance by implementing the method

abstract process(instance: any, environment: Environment): void

As an example the AOP processing is done with this mechanism!

Module

Classes annotated with @module determine which classes will be handled by a container.

Example:

@module({}) // implicitly {name: ""}
class ApplicationModule extends Module {

   ...
}

A module is a regular injectable class, so it can add additional factory methods as well.

The logic how classes relate to modules are quite different to a Spring-like logic, since Typescript doesn't remember file or directory locations, so a different approach is taken by matching module names and @injectable module references:

Example:

@module({name: "test"})
class TestModule extends Module {}

@injectable({scope: "test"})
class Test {}

TestModule in this case will only register Test.

As a default the module name and the @injectable module property is "". Another property imports: [...] may specify other modules ( by type ) recursively adding the respective names the the list of supported injectables.

Environment

The di container - called environment - is constructed, given a module:

let environment = new Environment({module: TestModule})
environment.start()

...


environment.stop()

starting and stopping an environment will trigger the corresponding lifecycle methods @onRunning and @onDestroy!

Different parameters are possible:

  • parent?: Environment an optional parent environment.

Setting a parent environment means that all of its providers are inherited.

Instance retrieval

The only method to retrieve objects is

 get<T>(type: new(...args: any[]) => T): T

Depending on the scope, it will either create a new instance or return a cached instance! Valid scopes are;

  • "singleton" object is created exactly once and cached
  • "request" object is recreated on ever get call.
  • "environment" object is recreated per environment,

If a base class is requested, it will return a concrete class, as long as there is only one registered implementing class. Otherwise an exception is thrown.

If an inherited object is requested and created, it will still stay in the defining environment ( the parent in that case ).

The exceptions are objects with scope "environment", which will be recreated even if the parent already owns an instance!

AOP

The biggest advantage in contrast to all other solutions is that it has an integrated aop mechanism which let's you modify functions with a fluent interface that decorate injectable classes.

@injectable()
class Aspects {}
    @around(methods().of(Service).thatAreAsync())
    async aroundMethod(invocation: Invocation): Promise<any> {
        ...
        try {
            return await invocation.proceed()
        } 
        finally {
            ...
        }
    }

    @after(methods().named("foo"))
    afterMethod(invocation: Invocation) {
        ...
    }

    @error(methods().of(Service))
    error(invocation: Invocation) {
        ...
    }
}

This of course is a perfect fit, since aspects typically require its own infrastructure which here is simply injected in the respective class. Different aspects are possible:

  • before aspects run before the targeted original methods
  • after aspects run after the targeted original methods
  • error aspects run in case the original targeted methods throw an error
  • around aspects run "around" the targeted original methods which means that the can add any logic prior to delegating to the original code.

Corresponding decorators with the same names are available. A fluent interface is used to filter the respective methods that will inherit the aspects by starting with a @methods() call and the following filters:

  • named(...expressions: string[]) matches methods as per regular expression
  • matching(...name: string[]) matches methods as per name
  • returning(type: Type<any>) matches the return type
  • of(type: Type<any>) matched the method class ( or subclass )
  • thatAreSync() matches sync methods
  • thatAreAsync() matches async methods
  • classDecoratedWith(decorator: ClassDecorator) matches class decorators
  • decoratedWith(decorator: MethodDecorator) matches method decorators

Every aspect is expected to have on argument of type Invocation. In case or around methods a proceed call must be executed.

Example:

 @around(...)
 aroundMethod(invocation: Invocation): any {
    try {
       ...
       return invocation.proceed() // or await in cxase of async arounds
     } 
     finally {
       ...
     }
}

Clone this wiki locally