Last active
March 22, 2019 17:17
-
-
Save HerringtonDarkholme/f11152e6e29145040fa4 to your computer and use it in GitHub Desktop.
typesafe DI in TypeScript
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
/// <reference path='./typings/tsd.d.ts' /> | |
import 'reflect-metadata' | |
type _ = {} | |
type ClassN<N, T> = { new (...a: N[]): T } | |
type FN8<A, B, C, D, E, F, G, H, R> = (a?: A, b?: B, c?: C, d?: D, e?: E, f?: F, g?: G, h?: H) => R | |
type CLS8<A, B, C, D, E, F, G, H, R> = { new (a?: A, b?: B, c?: C, d?: D, e?: E, f?: F, g?: G, h?: H): R} | |
class Token<T> { | |
private _tokenBrand | |
constructor(public name: string) {} | |
} | |
type Bindable<T> = Token<T> | ClassN<_, T> | |
const PARAMTYPE = 'design:paramtypes' | |
function getSignature<T>(cls: ClassN<T, _>): Array<Bindable<T>> { | |
return Reflect.getOwnMetadata(PARAMTYPE, cls).slice() || [] | |
} | |
const enum BindType { CLASS, VALUE, FACTORY } | |
class Binder<T, Base> { | |
dependencies: Bindable<Base>[] = [] | |
cacheable: boolean = false | |
private _type: BindType | |
private _cls: ClassN<any, T> = null | |
private _val: T | |
private _fn: Function | |
constructor(private _injector: Injector<Base>) {} | |
private _releaseInjector() { | |
let injector = this._injector | |
this._injector = null | |
return injector | |
} | |
toClass<A extends Base, B extends Base, C extends Base, D extends Base, E extends Base, F extends Base, G extends Base, H extends Base>(fn: CLS8<A, B, C, D, E, F, G, H, T>, a?: Bindable<A>, b?: Bindable<B>, c?: Bindable<C>, d?: Bindable<D>, e?: Bindable<E>, f?: Bindable<F>, g?: Bindable<G>, h?: Bindable<H>): Injector<Base | T> | |
toClass(cls: ClassN<Base, T>, ...deps: Bindable<Base>[]): Injector<Base | T> { | |
this._type = BindType.CLASS | |
this._cls = cls | |
this.dependencies = getSignature(cls) | |
deps.forEach((d, i) => { | |
if (d) this.dependencies[i] = d | |
}) | |
return this._releaseInjector() | |
} | |
toValue(val: T): Injector<Base | T> { | |
this._type = BindType.VALUE | |
this._val = val | |
this.cacheable = true | |
return this._releaseInjector() | |
} | |
toFactory<A extends Base, B extends Base, C extends Base, D extends Base, E extends Base, F extends Base, G extends Base, H extends Base>(fn: FN8<A, B, C, D, E, F, G, H, T>, a?: Bindable<A>, b?: Bindable<B>, c?: Bindable<C>, d?: Bindable<D>, e?: Bindable<E>, f?: Bindable<F>, g?: Bindable<G>, h?: Bindable<H>): Injector<Base | T> | |
toFactory(fn: (...a: Base[]) => T, ...deps: Bindable<Base>[]): Injector<Base | T> { | |
this._type = BindType.FACTORY | |
this._fn = fn | |
this.dependencies = deps | |
return this._releaseInjector() | |
} | |
resolve(deps: any[]): Promise<T> { | |
switch (this._type) { | |
case BindType.CLASS: | |
let cls = this._cls | |
return Promise.resolve(new cls(...deps)) | |
case BindType.VALUE: | |
return Promise.resolve(this._val) | |
case BindType.FACTORY: | |
return Promise.resolve(this._fn(...deps)) | |
} | |
} | |
} | |
export class Injector<Base> { | |
private _resolving = new Set<Bindable<_>>() | |
private _typeBinderMap = new Map<Bindable<_>, Binder<_, Base>>() | |
private _cache = new Map<Bindable<_>, Promise<any>>() | |
get<T extends Base>(cls: Bindable<T>): Promise<T> { | |
let binder = this._typeBinderMap.get(cls) | |
if (!binder) throw new NoBinding(cls['name']) | |
let cache = binder.cacheable ? this._cache.get(cls) : null | |
if (cache) { | |
return cache | |
} | |
if (this._resolving.has(cls)) throw new CyclicDependency(cls['name']) | |
this._resolving.add(cls) | |
let depPromise = binder.dependencies.map(dep => this.get(dep)) | |
this._resolving.delete(cls) | |
let ret = Promise.all(depPromise).then(args => binder.resolve(args)) | |
if (binder.cacheable) this._cache.set(cls, ret) | |
return ret | |
} | |
bind<T>(cls: Bindable<T>): Binder<T, Base> { | |
let binder = new Binder<T, Base>(this) | |
this._typeBinderMap.set(cls, binder) | |
return binder | |
} | |
bindWithCache<T>(cls: Bindable<T>): Binder<T, Base> { | |
let binder = this.bind<T>(cls) | |
binder.cacheable = true | |
return binder | |
} | |
use<A>( a: ClassN<Base, A>): Injector<A|Base>{ | |
return this.bind(a).toClass(a) | |
} | |
useWithCache<A>( a: ClassN<Base, A>): Injector<A|Base>{ | |
return this.bindWithCache(a).toClass(a) | |
} | |
static create(): Injector<Injector<_>> { | |
let inj = new Injector<Injector<_>>() | |
inj.bind(Injector).toValue(inj) | |
return inj | |
} | |
static inherit<A>(p: Injector<A>): Injector<A> { | |
let inj = new Injector<A>() | |
p._typeBinderMap.forEach((val, key) => { | |
inj._typeBinderMap.set(key, val) | |
}) | |
return inj | |
} | |
} | |
export class CyclicDependency extends Error { | |
constructor(tokenName: string) { | |
super(`Cyclic Dependency: ${tokenName}`) | |
} | |
} | |
export class NoBinding extends Error { | |
constructor(tokenName: string) { | |
super(`No Binding Provided: ${tokenName}`) | |
} | |
} | |
export function token<T>(name: string): Token<T> { | |
return new Token(name) | |
} | |
export function Inject(a: any): void {} |
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
/// <reference path='../typings/tsd.d.ts' /> | |
import {Inject, Injector, token, NoBinding, CyclicDependency} from '../index' | |
import * as assert from 'assert' | |
@Inject | |
class Customer { | |
private brand | |
constructor() {} | |
} | |
@Inject | |
class Clerk { | |
private brand | |
constructor() {} | |
} | |
@Inject | |
class Shop { | |
private shopBrand | |
constructor(public c: Clerk) {} | |
static test(a: number) {} | |
} | |
class B { | |
} | |
@Inject | |
class A { | |
constructor(b: B) {} | |
} | |
var clerkToken = token<Clerk>('clerk') | |
describe('di', () => { | |
it('should compile', done => { | |
var injector = Injector.create() | |
var b = injector | |
.bind(clerkToken).toClass(Clerk) | |
.bind(Shop).toClass(Shop, clerkToken) | |
.get(Shop) | |
b.then(s => { | |
assert(s instanceof Shop) | |
assert(s.c instanceof Clerk) | |
}).then(done) | |
}) | |
it('should throw unresolved token', () => { | |
var injector = Injector.create() | |
assert.throws(_ => { | |
var b = injector | |
.use(Customer) | |
.bind(clerkToken).toClass(Clerk) | |
.bind(Shop).toClass(Shop) | |
.get(Shop) | |
}, NoBinding) | |
}) | |
it('should throw cyclic error', () => { | |
var injector = Injector.create() | |
assert.throws(_ => { | |
var b = injector | |
.use(A) | |
.bind(B).toFactory((a) => new B(), A) | |
.get(A) | |
}, CyclicDependency) | |
}) | |
it('should return new instance', done => { | |
var injector = Injector.create() | |
var b = injector | |
.use(Customer) | |
Promise.all([b.get(Customer), b.get(Customer)]).then(([a,b]) => { | |
assert(a instanceof Customer) | |
assert(b instanceof Customer) | |
assert(a !== b) | |
}) | |
.then(done) | |
}) | |
it('should return same instance when cached', done => { | |
var injector = Injector.create() | |
var b = injector | |
.useWithCache(Customer) | |
Promise.all([b.get(Customer), b.get(Customer)]).then(([a,b]) => { | |
assert(a instanceof Customer) | |
assert(b instanceof Customer) | |
assert(a === b) | |
}) | |
.then(done) | |
}) | |
it('should return cached instance in deep', done => { | |
var injector = Injector.create() | |
var b = injector | |
.useWithCache(Clerk) | |
.use(Shop) | |
Promise.all([b.get(Shop), b.get(Shop)]).then(([a,b]) => { | |
assert(a instanceof Shop) | |
assert(b instanceof Shop) | |
assert(a !== b) | |
assert(a.c === b.c) | |
}) | |
.then(done) | |
}) | |
it('should inherit parent', done => { | |
var injector = Injector.create() | |
var a = injector | |
.use(Customer) | |
.use(Clerk) | |
.use(Shop) | |
var b = Injector.inherit(a) | |
b.get(Customer).then(a => { | |
assert(a instanceof Customer) | |
}) | |
.then(done) | |
}) | |
it('should override parent', done => { | |
var injector = Injector.create() | |
var a = injector | |
.use(Customer) | |
.use(Clerk) | |
.use(Shop) | |
var b = Injector.inherit(a) | |
var clerk = new Clerk | |
b.bind(Clerk).toValue(clerk) | |
b.get(Shop).then(a => { | |
assert(a instanceof Shop) | |
assert(a.c === clerk) | |
}) | |
.then(done) | |
}) | |
it('should use token', done => { | |
var injector = Injector.create() | |
var tkn = token<number>('test') | |
var b = injector | |
.bind(tkn).toValue(456) | |
b.get(tkn).then(a => { | |
assert(a === 456) | |
}) | |
.then(done) | |
}) | |
}) |
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
{ | |
"version": "v4", | |
"repo": "borisyankov/DefinitelyTyped", | |
"ref": "master", | |
"path": "typings", | |
"bundle": "typings/tsd.d.ts", | |
"installed": { | |
"es6-collections/es6-collections.d.ts": { | |
"commit": "3d2f9971a107e2eee2de3ec5318f5c54071e16ed" | |
}, | |
"es6-promise/es6-promise.d.ts": { | |
"commit": "3d2f9971a107e2eee2de3ec5318f5c54071e16ed" | |
}, | |
"mocha/mocha.d.ts": { | |
"commit": "6d3c9f422fe385f4f34d3be4c7920476163c87ec" | |
}, | |
"node/node.d.ts": { | |
"commit": "6d3c9f422fe385f4f34d3be4c7920476163c87ec" | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
To fully support type-safe DI, a compiler extension or a code generator is needed. Java's DI relies on annotations and code generation.
Maybe Babel can do this right now. But TypeScript still needs a long way to go for a customizable emitter.