Don't forget to enable experimentalDecorators in tsconfig.json !
Created
June 13, 2018 21:05
-
-
Save 422404/91c2220197035d678cf1df7eceae4993 to your computer and use it in GitHub Desktop.
JavaScript Dependency Injection™ using Typescript decorators
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*********************************** Typing ***************************************/ | |
/** | |
* Signature of a constructible class | |
*/ | |
interface IClass { | |
/** | |
* Used to express the constructability of a class | |
*/ | |
new(...args: any[]): {}; | |
} | |
/** | |
* Signature of a service factory class | |
*/ | |
interface IServiceFactory extends IClass { | |
/** | |
* Construct and return the instance of the class constructed by the factory | |
* @param {any} args Factories are free to require any args | |
* @returns {Object} Newly created instance | |
*/ | |
getInstance: (...args: any[]) => Object; | |
} | |
/** | |
* Signature of a registered service | |
* Handful for IntelliSence | |
*/ | |
interface IService extends IClass { | |
/** | |
* Service identifier | |
* Not really optionnal, just because classes are modified at runtime | |
* @type {UID} | |
*/ | |
$$uid?: UID, | |
/** | |
* Must be equal to "service" | |
* Not really optionnal, just because classes are modified at runtime | |
* @type {string} | |
*/ | |
$$type?: 'service' | |
} | |
/** | |
* Config that ndicate which factory to use to create the service instance the | |
* consumer depends on | |
*/ | |
type IServiceFactoryConfig = { factory: IServiceFactory, args?: any[] } | |
/** | |
* Config that indicate the service instance or factory to use to create | |
* the service instance the consumer depends on | |
*/ | |
type IServiceInjectionConfig = IService | IServiceFactoryConfig | |
/** | |
* A unique identifier | |
*/ | |
type UID = number | |
/*********************************** Private **************************************/ | |
/** | |
* Injects arguments into factory "getInstance" method | |
* @param {any} args Arguments to inject, only services in this case | |
* @param {IServiceFactory} factoryClass Factory's class | |
* @returns {IServiceFactory} The factory class with injected args into "getInstance" method | |
*/ | |
function injectArgsIntoFactory(args: any[], factoryClass: IServiceFactory): IServiceFactory { | |
let getInstanceFn: Function = factoryClass.getInstance; | |
if (!getInstanceFn) { | |
throw 'The given class is not a factory !'; | |
} | |
factoryClass.getInstance = getInstanceFn.bind(factoryClass, ...args); | |
return factoryClass; | |
} | |
/** | |
* Injects arguments into a constructor | |
* @param {any} args Arguments to inject into the constructor, only services in this case | |
* @param {IClass} baseClass Class to inject the args into | |
* @returns {IClass} The new class with the args injected into its constructor | |
*/ | |
function injectArgsIntoConstructor(args: Object[], baseClass: IClass): IClass { | |
return class extends baseClass { | |
constructor(..._args: any[]) { | |
super(...args, ..._args); | |
} | |
}; | |
} | |
/** | |
* Collects the services instances required by the config | |
* @param {IServiceInjectionConfig[]} dependanciesConfig Config containing the services | |
* or factories to use to retrieve the services instances | |
* @returns {Object[]} The services instances | |
*/ | |
function collectServices(dependanciesConfig: IServiceInjectionConfig[]): Object[] { | |
return dependanciesConfig.map(service => constructServiceOrDelegateToFactory(service)); | |
} | |
/** | |
* Constructs a service from the config | |
* @param {IServiceInjectionConfig} dependencyConfig Service class or service factory config | |
* @returns {Object} The service instance | |
*/ | |
function constructServiceOrDelegateToFactory(dependencyConfig: IServiceInjectionConfig): Object { | |
let serviceInstance: Object; | |
if (typeof dependencyConfig === 'object') { | |
serviceInstance = dependencyConfig.factory.getInstance.apply(null, | |
dependencyConfig.args !== undefined ? dependencyConfig.args : [] | |
); | |
} else if (typeof dependencyConfig === 'function') { | |
if (!dependencyConfig.$$uid || !dependencyConfig.$$type || dependencyConfig.$$type != 'service') { | |
throw 'Service not registered !'; | |
} | |
let serviceCtor: IClass = <IClass>ServiceRegistry.getService(dependencyConfig.$$uid); | |
serviceInstance = new serviceCtor(); | |
} else { | |
throw 'Bad config !'; | |
} | |
return serviceInstance; | |
} | |
/** | |
* Represents a registered service | |
* @class | |
*/ | |
class RegisteredService { | |
/** | |
* @constructor | |
* @param {UID} uid Unique identifier of the registered service | |
* @param {IClass} klass Service's class | |
*/ | |
constructor(public uid: UID, public klass: IClass) { | |
} | |
} | |
/** | |
* Registry of services | |
* @class | |
*/ | |
class ServiceRegistry { | |
/** | |
* The registered services | |
* @static | |
* @type {RegisteredService[]} | |
*/ | |
private static services: RegisteredService[] = []; | |
/** | |
* Current uid to be used | |
* @static | |
* @type {UID} | |
*/ | |
private static currentUID: UID = 0; | |
/** | |
* Generates a new uid and returns it | |
* @static | |
* @returns {UID} The new uid | |
*/ | |
static generateUID() { | |
return ++ServiceRegistry.currentUID; | |
} | |
/** | |
* Registers a service | |
* @param {IClass} service Service's class | |
* @returns {IClass} The new class with $$uid and $$type properties added | |
*/ | |
static registerService(service: IClass): IClass { | |
let uid = ServiceRegistry.generateUID(); | |
(<any>service).$$uid = uid; | |
(<any>service).$$type = 'service'; | |
ServiceRegistry.services.push(new RegisteredService(uid, service)); | |
return service; | |
} | |
/** | |
* Returns the class of a registered service | |
* @param {UID} uid The unique id of the service | |
* @returns {IClass} The registered service's class | |
*/ | |
static getService(uid: UID): IClass { | |
let service: RegisteredService|undefined = ServiceRegistry.services.find(service => | |
service.uid == uid | |
); | |
if (!service) { | |
throw 'Unregistered service !'; | |
} | |
return service.klass; | |
} | |
}; | |
/*********************************** Public ***************************************/ | |
/** | |
* Service decorator | |
* Used to register a class as a service | |
*/ | |
export function Service(): Function { | |
return function (constructor: IClass): IClass { | |
return ServiceRegistry.registerService(constructor); | |
}; | |
} | |
/** | |
* Service consumer decorator | |
* Performs dependancies (services) injection into a class constructor | |
* @param {IServiceInjectionConfig[]} needs Array of services or services factories config | |
* Array items can be either services classes (i.e: UserService) or objects thats specifies | |
* the factory to use and its optionnal arguments (i.e: { factory : UserServiceFactory [, args: [some arg, another]] }) | |
*/ | |
export function ServicesConsumer(needs: IServiceInjectionConfig[]): Function { | |
if (!needs) { | |
throw 'To few arguments given !'; | |
} | |
return function (constructor: IClass): IClass { | |
let services: Object[] = collectServices(needs); | |
return injectArgsIntoConstructor(services, constructor); | |
}; | |
} | |
/** | |
* Service factory decorator | |
* Indicates that a static class is a service factory (do not register it) and | |
* performs dependancies injection into the "getInstance" method used to construct | |
* the provided service | |
* @param needs Array of services or services factories config | |
* Array items can be either services classes (i.e: UserService) or objects thats specifies | |
* the factory to use and its optionnal arguments (i.e: { factory : UserServiceFactory [, args: [some arg, another]] }) | |
*/ | |
export function ServiceFactory(needs?: IServiceInjectionConfig[]): Function { | |
if (!needs) { | |
return; | |
} | |
return function (constructor: IServiceFactory): IServiceFactory { | |
let services: Object[] = collectServices(needs); | |
return injectArgsIntoFactory(services, constructor); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Service, ServicesConsumer, ServiceFactory } from './dependencyInjection'; | |
/** | |
* Whether or not the user is logged | |
* (such authentication) | |
*/ | |
const AUTHENTICATED = false; | |
/** | |
* Base language of the strings | |
*/ | |
const LANG = 'fr'; | |
/** | |
* Base strings in English and their translation in French and Spanish | |
*/ | |
const strings: Object = { | |
'<anonymous>': { | |
'fr': '<anonyme>', | |
'es': '<anónimo>' | |
}, | |
'Johnatan': null, | |
'Hello': { | |
'fr': 'Salut', | |
'es': 'Hola' | |
} | |
}; | |
/** | |
* Internationalization service | |
* @class | |
*/ | |
@Service() | |
class I18nService { | |
private baseLang: string = 'en'; | |
/** | |
* @constructor | |
* @param {string} [lang] Language to translate the text into | |
* @param {Object} [strings] Object that contains base strings and their translations | |
*/ | |
constructor(private lang?: string, private strings?: any) {} | |
/** | |
* Translate a text into other languages | |
* @param text Text to translate | |
* @returns {string} translated text or base text if no translations | |
*/ | |
$(text: string): string { | |
if (!this.lang | |
|| !this.strings | |
|| this.lang == this.baseLang | |
|| !this.strings[text] | |
|| !this.strings[text][this.lang]) { | |
return text; | |
} | |
return this.strings[text][this.lang]; | |
} | |
} | |
/** | |
* Basic authentication service for the purpose of testing | |
* @class | |
*/ | |
@Service() | |
class AuthService { | |
/** | |
* Returns whether or not the user is authenticated | |
* @ | |
*/ | |
isAuthenticated(): boolean { | |
return AUTHENTICATED; | |
} | |
} | |
/** | |
* Constructs I18nService instance with more control | |
* @class | |
*/ | |
@ServiceFactory() | |
class I18nServiceFactory { | |
/** | |
* Creates an instance of I18nService | |
* @static | |
* @param {string} lang Language used to translate text | |
* @returns {Object} I18nService instance | |
*/ | |
static getInstance(lang: string): Object { | |
return new I18nService(lang, strings); | |
} | |
} | |
/** | |
* Constructs UserService instance with more control | |
* @class | |
*/ | |
@ServiceFactory([ | |
AuthService, | |
{ factory: I18nServiceFactory, args: [LANG] } | |
]) | |
class UserServiceFactory { | |
/** | |
* Creates an instance of UserService | |
* @static | |
* @param {AuthService} auth Some authentification service | |
* @param {I18nService} i18n I18n service dependency used to print internationalized text | |
*/ | |
static getInstance(auth: AuthService, i18n: I18nService): Object { | |
return new UserService(i18n, auth.isAuthenticated() ? i18n.$('Johnatan') : null); | |
} | |
} | |
/** | |
* Cool service sayin' "Hello" | |
* @class | |
*/ | |
@Service() | |
@ServicesConsumer([ | |
{ factory: I18nServiceFactory, args: [LANG] } | |
]) | |
class HelloService { | |
/** | |
* @constructor | |
* @param {I18nService} i18n I18n service dependency used to print internationalized text | |
*/ | |
constructor(private i18n: I18nService) {} | |
/** | |
* Say hello ! | |
* @returns {string} the "Hello" string translated or not | |
*/ | |
sayHello(): string { | |
return this.i18n.$('Hello'); | |
} | |
}; | |
/** | |
* Service providing informations about an user (logged or not) | |
* @class | |
*/ | |
@Service() | |
class UserService { | |
/** | |
* @constructor | |
* @param {I18nService} i18n I18n service dependency used to print internationalized text | |
* @param {string} [username] name of the logged in user | |
*/ | |
constructor(private i18n: I18nService, private username?: string) {} | |
/** | |
* Shows the current user's name | |
* @returns {string} current user's name | |
*/ | |
showUser(): string { | |
return this.username ? this.username : this.i18n.$('<anonymous>'); | |
} | |
} | |
/** | |
* Some component using dependency injection | |
* @class | |
*/ | |
@ServicesConsumer([ | |
HelloService, | |
{ factory: UserServiceFactory } | |
]) | |
class MyComponent { | |
/** | |
* @constructor | |
* @param {HelloService} [hello] Some hello service dependency | |
* Just for the test it's made optionnal as we construct the component by hand | |
* @param {User} [user] Some user service dependency | |
* Just for the test it's made optionnal as we construct the component by hand | |
*/ | |
constructor(hello?: HelloService, user?: UserService) { | |
console.log(hello.sayHello(), user.showUser(), '!'); | |
} | |
} | |
// Should be delegated to a component builder | |
let component: MyComponent = new MyComponent(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment