Created
October 4, 2018 04:28
-
-
Save mattmccray/d81f56373cb57e7ab9a8971b1f285c13 to your computer and use it in GitHub Desktop.
Keep It Stupid Simple: TypeScript DI
This file contains 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
const _constructing: Set<any> = new Set() | |
const _services: Map<any, any> = new Map() | |
const _serviceOverrides: Map<any, any> = new Map() | |
interface Type<T> { new(...args: any[]): T } | |
function createInstance(CTor: any, args: any[] = []): any { | |
if (!CTor) { | |
// debugger | |
throw new Error(`Null or undefined dependency specified.`) | |
} | |
if (_constructing.has(CTor) && args && args.length == 0) { | |
console.warn(`Circular reference to service!`, CTor) | |
throw new Error(`Circular reference to service!`) | |
} | |
_constructing.add(CTor) | |
const instance = new CTor(...args) | |
_constructing.delete(CTor) | |
return instance | |
} | |
/** | |
* Resolves a class into an instance. If the class is marked as a service, all requests for the | |
* class will return the same instance. Otherwise a new instance is created and returned. You may | |
* pass arguments for the class constructor, but if it's a service, the resulting instance won't | |
* be cached as a singleton. | |
*/ | |
export function resolve<T>(definition: Type<T>, ...args: any[]): T { | |
const isService = _services.has(definition) | |
let instance = null | |
if (isService) { | |
if (args.length > 0) { | |
console.warn("Services don't accept parameters, this instance won't be cached!", args) | |
} | |
else { | |
instance = _services.get(definition) | |
} | |
} | |
if (instance == null) { | |
instance = createInstance(definition, args) | |
if (isService && args.length == 0) { | |
_services.set(definition, instance) | |
} | |
} | |
return instance as T | |
} | |
/** | |
* Property decorator to enable lazy instantiation of properties. | |
*/ | |
export function inject(definition: Type<any>, ...args: any[]) { | |
return function (target: any, propertyKey: string) { | |
if (!definition) { | |
throw new Error("Injection must define a source.") | |
} | |
Object.defineProperty(target, propertyKey, { | |
enumerable: true, | |
configurable: true, | |
get() { | |
const instance = resolve(definition, ...args) | |
Object.defineProperty(target, propertyKey, { value: instance }) | |
return instance | |
} | |
}) | |
} | |
} | |
/** | |
* Property decorator to enable lazy instantiation of properties, accepts a type factory function. | |
*/ | |
export function injectDynamic(definition: () => Type<any>, ...args: any[]) { | |
return function (target: any, propertyKey: string) { | |
if (!definition) { | |
throw new Error("Dynamic injection must define a source factory.") | |
} | |
Object.defineProperty(target, propertyKey, { | |
enumerable: true, | |
configurable: true, | |
get() { | |
const definitionTarget = definition() | |
const instance = resolve(definitionTarget, ...args) | |
Object.defineProperty(target, propertyKey, { value: instance }) | |
return instance | |
} | |
}) | |
} | |
} | |
/** | |
* Decorator that creates a getter property that will call resolve on every access. Only for use | |
* with services, makes them hot-swappable. | |
*/ | |
export function injectLive(definition: Type<any>) { | |
return function (target: any, propertyKey: string) { | |
if (!definition) { | |
throw new Error("Injection must define a source.") | |
} | |
Object.defineProperty(target, propertyKey, { | |
get: () => resolve(definition) | |
}) | |
} | |
} | |
/** | |
* Decorator to mark a class as a service. | |
*/ | |
export function markAsService<T = any>(source: T): T { | |
_services.set(source, null) | |
return source | |
} | |
/** | |
* Override a service binding... Must be an instance! | |
*/ | |
function bindInstance(source: any, target: any) { | |
_serviceOverrides.set(source, _services.get(source)) | |
_services.set(source, target) | |
} | |
/** | |
* Restores a previously overridden service binding to its previous value | |
*/ | |
function restoreInstanceBinding(source: any) { | |
_services.set(source, _serviceOverrides.get(source)) | |
_serviceOverrides.delete(source) | |
} | |
function hasInstance(source: any) { | |
return _services.has(source) && _services.get(source) != null | |
} | |
function allServices() { | |
const map: any = {} | |
_services.forEach((service: any, key: any) => { | |
map[getFunctionName(key)] = service | |
}) | |
return map | |
} | |
const tools = { | |
allServices, | |
bindInstance, | |
hasInstance, | |
restoreInstanceBinding, | |
} | |
export const DI = { | |
inject, | |
injectDynamic, | |
injectLive, | |
markAsService, | |
resolve, | |
tools, | |
} | |
export default DI | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment