Skip to content

Instantly share code, notes, and snippets.

@mattmccray
Created October 4, 2018 04:28
Show Gist options
  • Save mattmccray/d81f56373cb57e7ab9a8971b1f285c13 to your computer and use it in GitHub Desktop.
Save mattmccray/d81f56373cb57e7ab9a8971b1f285c13 to your computer and use it in GitHub Desktop.
Keep It Stupid Simple: TypeScript DI
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