2.0.0 • Published 2 years ago

@kuroi/syringe v2.0.0

Weekly downloads
-
License
ISC
Repository
github
Last release
2 years ago

Syringe Dependency Injection

Syringe is a custom dependency injection framework meant for TypeScript projects. It aims to require minimal effort from the developer by not requiring manual mapping code or any dependencies.

Syringe is best suited for applications with simple lifecycles and lots of singletons, as it was developed for use in a bot, and the code has not been production tested against more advanced use cases.

Usage

Importing

Use ES6 imports to access the Syringe namespace.

import { Syringe } from '@kuroi/syringe'

Creating Injectable Classes

Mark classes as injectable entities with the @Injectable decorator. If your class is a singleton, provide a scope of 'global', otherwise leave the decorator argument blank to allow Syringe to construct a new instance for each injection.

Singleton

@Syringe.Injectable({
  scope: 'global'
})
export class MyService implements IService {
  // ...
}

Non-singleton

@Syringe.Injectable()
export class MyInstance {
  // ...
}

Note: The @Injectable decorator will not work alongside class decorators that wrap the target class in an anonymous extending class. If your decorator returns a Proxy to the target constructor or manually preserves the prototype information (particularly the name), Syringe DI should still work, but it's advised that you put the @Injectable decorator first.

@Syringe.Injectable()
@Wrapper
export class MultiDecorated {
	// ...
}

Injecting Classes

To inject your @Injectable class as a dependency, use @Syringe.Inject to automatically supply the dependency instance to another @Injectable constructor. The decorator requires either the class definition for which you want to inject an instance or a direct reference to an InjectionToken as an argument.

@Syringe.Injectable()
export class MyComponent {
  constructor(@Syringe.Inject(MyService) private service: IService) {
    // ...
  }
}

Creating Injection Tokens

Sometimes, you might want to inject constants, function calls, or other non-class entities into your @Injectable class. In order to do this, you must manually construct an InjectionToken and return your value from its factory function.

const _someConstant: string = 'Jerry, hello! It\'s me, Uncle Leo!'
export const UNCLE_LEO = new InjectionToken('UncleLeo', {
  scope: 'global',
  factory: () => _someConstant
})

// inject an ID generator function
const GENERATE_ID = new InjectionToken('GenerateId', {
  scope: 'global',
  factory: () => () => Utilities.generateId()
})

Then, you can inject this token directly into an @Injectable class.

@Syringe.Injectable()
export class MyClass {
  public id: string;
  constructor(
    @Syringe.Inject(GENERATE_ID) idGenerator: () => string,
    @Syringe.Inject(UNCLE_LEO) private greeting: string
  ) {
    this.id = idGenerator()
  }
  public greet(): void {
    console.log(this.greeting) // Jerry, hello! It's me, Uncle Leo!
  }
}

Bootstrapping

To get all of your classes actually running, simply tell Syringe to inject the top level class(es) at the entry point(s) of your application, usually in index.ts or similar. Syringe will automatically construct all of its dependencies, and your instance is ready to go.

const app = Syringe.inject<MyApp>(MyApp)
app.start()

Providers

Sometimes, you may want to inject an abstract class and specify a concretion at a higher level context, or just substitute one implementation for another. Such is the beauty of dependency injection.

To do so, simply include a provider in the arguments to Syringe.inject when bootstrapping an entry point to your application.

// inject Abstraction
@Syringe.Injectable()
export class MyClass {
  constructor(@Syringe.Inject(AbstractService) service: AbstractService) {}
}

const app = Syringe.inject<MyApp>(MyApp, {
  providers: [
	NonDecoratedClass, // inject singletons that aren't decorated with @Injectable
	{
      for: AbstractService,
      use: ConcreteService // extension type of AbstractService to provide for MyApp
    },
	{
      for: AnotherAbstractService,
	  instance: PreConstructedValueType // extension value of AnotherAbstractService to provide for MyApp
	}
  ]
})

Lifecycles

There are two main lifecycle hooks available to all entities managed by Syringe: OnInit and OnDestroy. Syringe's container will automatically call these methods if implemented during construction and teardown.

To hook into these lifecycles, implement the interfaces Syringe.OnInit and/or Syringe.OnDestroy.

@Syringe.Injectable()
export class MyLifecycleClass implements Syringe.OnInit, Syringe.OnDestroy {
  public onInit(): void {
    // ...
  }
  public onDestroy(): void {
    // ...
  }
}

Credits / Considerations

This library is largely inspired by the feel of Google Angular's DI framework, minus the custom module pattern and any external dependencies. You can use it for UI/browser apps, but it's better suited for server side Node.js/TypeScript apps.

2.0.0

2 years ago

1.0.4

3 years ago

1.0.3

3 years ago

1.0.2

3 years ago

1.0.1

3 years ago

1.0.0

3 years ago