Skip to content

Bloat-free and magical IoC-container for Typescript based on code-generation

License

Notifications You must be signed in to change notification settings

MrOnlineCoder/genioc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

genioc

Bloat-free and magical IoC-container for Typescript based on code-generation

How it works?

For genioc to work, there is additional pre-processing step before running your app.

genioc compiles your Typescript project, parses dependencies of each class constructor, and generates a specific-for-you-project file which exports a ready-to-use IoC container for you (example here).

Please note, that it's not the same as constructor signature parsing as for example awilix does - with genioc you may use whatever names or order of constructor arguments you want - only the type of them matters.

Example usage:

//userService.ts
export class UserService {
  constructor(
    private readonly userRepository: IUserRepository
  ) {
    
  }
}
//index.ts
import container from './genioc.autogenerated.ts'

container.bind("IUserRepository", SqlUserRepository);
container.bindSelf(UserService);

const userService: UserService = container.get(UserService);

userService.doStuff(); //at this point, userService would be a valid instance with SqlUserRepository implementation injected

Because of current Typescript limitations, you must pass interface names as strings to bind them. However, genioc creates a union special type for that to guarantee that you will enter a valid interface name.

Why genioc

In Java or PHP, because of powerful built-in reflection features, you can inject dependencies into constructors just by specifying correct type:

class UserService {
    private final IUserRepository userRepo;

    public UserService(IUserRepository userRepo) {
        this.userRepo = userRepo;
    }
}

in current TS/JS solutions, you will have either to:

  1. use tokens with decorators (via old reflect-metadata):
const UserRepositoryIdentifier = Symbol.for('UserRepository')

class UserService {
    constructor(
        @inject(UserRepositoryIdentifier) private readonly userRepo: IUserRepository
    ) {

    }
}
  1. in such solutions as awilix with InjectMode.CLASSIC or for example with AngularJS injector, which just stringify function definition and parse argument names, you must preserve the original names of the arguments in the code, and therefore cannot minify the code.

  2. destruct object in the constructor which contains all dependencies, which basically is a violation of IoC principle and resolves to Service locator pattern

class UserService {
  constructor(opts) {
    this.emailService = opts.emailService // this is not true DI!
    this.logger = opts.logger
  }
}

And as you might see, all these solutions are kinda hacky and not as magical and easy-to-use as Java one for example.

genioc on other hand, allows you to write Java-way injection:

class UserService {
    constructor(
        private readonly userRepo: IUserRepository
    ) {

    }
}

Yes, that is really that simple. Just do not forget to bind the actual IUserRepository to an implementation.

However, there is one catch - you must run genioc script everytime you add a new dependency in the code. And to find out why, now we come to the mechanism how genioc works.

Usage in project

Due genioc being not quite a default library, you must alter your development workflow a little:

  1. First you define interfaces of your dependencies such as IUserRepository, IMailer or ILogger, etc..
  2. Then you create actual implementations for them: SqlUserRepository, MailchimpMailer, ConsoleLogger
  3. Then you pass interfaces for these dependencies to classes where you need them, for example:
class MyService {
    constructor(
        private readonly mailer: IMailer,
        private readonly userRepo: IUserRepository
    ) {

    }

    public resetPassword(email: string) {
        const user = await this.useRepo.findByEmail(email);

        await mailer.sendResetPasswordEmail(user.email, user.resetToken);
    }
}
  1. You run genioc to generate IoC container code for you
  2. You bind interfaces to their dependencies:
import container from './genioc.autogenerated.ts'

container.bind('ILogger', ConsoleLogger);
container.bind('IMailer', MailchimpMailer);
container.bind('IUserRepository', SqlUserRepository);
  1. Done. You can now run your app

Container API

genioc container is default-exported from generated file, so you should use the following import statement as a starting point:

import container from './genioc.autogenerated.ts'

All methods support either classes, strings or Symbols as tokens.

Following methods are supported:

container.bind(token, classConstructor);

Binds token to class classConstructor.

interface IDog {
    bark(): string;
}

class Doggy implements IDog {
    public bark() {
        return "Woof!";
    }
}

//...

container.bind("IDog", Doggy);

container.bindSelf(classConstructor);

Binds given class to itself, for injecting in the following manner:

container.bindSelf(MyClass);

//...
class MyClassUser {
    constructor(
        private readonly myClass: MyClass
    ) {

    }
}

container.bindValue<T>(token, value: T)

Binds token to given constant value. You can use optional generic type T to make sure the correct value type is passed. Useful for binding static objects such as configs or messages collections.

interface IAppConfig {
    niceDog: boolean;
} 

container.bindValue<IAppConfig>("IAppConfig", {
    niceDog: true
});

container.bindFactory<T>(token, factory: () => T)

Binds token to a factory function, which will be called every time you try to obtain an instance by this token.

interface ICurentTimeProvider {
    time: number;
} 

container.bindFactory<ICurentTimeProvider>("ICurentTimeProvider", () => {
    return {
        time: Date.now()
    }
});

container.get<T>(token)

Use to obtain instance of binding by token.

By default, it uses lazy resolving, which means that if instance does not exist, it will be resolved in-place and then cached for future use.

Will throw an error if you are trying to query token which has no assigned binding.

You should use generic argument T to explicitly define the return type of the function.

const app: Application = container.get<Application>("IApplication");

app.run();

Command line usage

genioc [--dev] [--watch | -w] [--output | -o <output path>] [project directory]
  • --dev - only used when testing genioc example itself
  • --watch | -w - starts in watch mode, which will automatically rebuild your IoC container when source code is changed
  • --output | -o <path> - specify name/path for output container file, relative to project directory
  • project directory - path to your project with tsconfig.json. If not used, it will use current process working directory.

Run this tool to re-build dependency tree for your project. You can run this before each build or just use it in watch mode.

Circular dependencies

genioc container is capable of resolving circular dependencies automatically, you can check the example. No additional decorators or wrappers needed.

Ways of injection

The main way of injection a dependency is using the constructor injection, and it should be the preferred one.

You can resolve them also in runtime by using container.get method, however, again, this is not dependency injection in classical form then.

Author

Nikita Kogut

License

MIT (see LICENSE file)

About

Bloat-free and magical IoC-container for Typescript based on code-generation

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published