Last active
February 9, 2022 13:13
-
-
Save castarco/f3d9723889d6529a5034c9dbd5dde841 to your computer and use it in GitHub Desktop.
A "new" pattern to create fully-typed registries/containers!
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
// This first file contains the first iteration of this pattern. It's already | |
// nice, but not enough for full-fledged IoC libraries. | |
// There's a second file in this same gist that goes one step further, although, | |
// for now, it still needs some extra polish. | |
// ----------------------------------------------------------------------------- | |
// First: the two main interfaces. | |
// They are the core of the pattern. | |
// ----------------------------------------------------------------------------- | |
export interface WritableRegistry { | |
register<K extends string | symbol, V>( | |
key: K, | |
value: V, | |
): asserts this is ReadableRegistry<K, V> | |
} | |
export interface ReadableRegistry<K extends string | symbol, V> { | |
get(key: K): V | |
} | |
// ----------------------------------------------------------------------------- | |
// Second: We need an actual implementation of the registry. | |
// | |
// This is a simple class that only implements the WritableRegistry interface, | |
// plus a "weak version" of the ReadableRegistry interface. | |
// ----------------------------------------------------------------------------- | |
class DummyRegistry implements WritableRegistry { | |
private items: Record<string | symbol, unknown> = {} | |
public register<K extends string | symbol, V>( | |
key: K, | |
value: V, | |
): asserts this is ReadableRegistry<K, V> { | |
this.items[key] = value | |
} | |
public get<K extends string | symbol>(key: K): unknown { | |
return this.items[key] | |
} | |
} | |
// ----------------------------------------------------------------------------- | |
// Third: registry instantiation | |
// | |
// When we instantiate a new registry, we MUST "hide" it behind the | |
// WritableRegistry interface. | |
// ----------------------------------------------------------------------------- | |
const r: WritableRegistry = new DummyRegistry() | |
// ----------------------------------------------------------------------------- | |
// Fourth: the registry in action | |
// ----------------------------------------------------------------------------- | |
// We can register whatever we want in the registry. | |
r.register('mynumber', 42) | |
r.register('mystring', 'the answer to everything') | |
// Now, we can access the value we registered, and we'll get the correct type | |
const v1 = r.get('mynumber') // typeof v1 === number (known at compile time) | |
const v2 = r.get('mystring') // typeof v2 === string (known at compile time) | |
// If we try to access something that we did not register, then the type checker | |
// will complain! :D | |
const invalid = r.get('not_registered') // compile-time type error! :D |
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
// This file contains "almost the same" as the previous one, but it adds a new feature: | |
// Now, it's "aware" of previous injected dependencies, so it does not allow to register | |
// factories depending on stuff that is not there yet. | |
// | |
// For now, this is just a temporary solution, as I didn't bake in yet the case of multiple | |
// simultaneous dependencies, but it's doable. It's just that I didn't have more time for | |
// now. | |
// | |
// Please, don't mind that this code is a bit less clean than the previous one, it's still | |
// in experimental stage. | |
export interface XyzWritableRegistry { | |
register<K extends string | symbol, V>( | |
key: K, | |
value: V extends () => infer _ | |
? V | |
: V extends (c: XyzReadableRegistry<infer K2, infer U2>) => infer _ // TODO: Generalize to more dependencies | |
? this extends XyzReadableRegistry<K2, U2> | |
? V | |
: never | |
: V, | |
): asserts this is XyzReadableRegistry< | |
K, | |
V extends () => infer U | |
? U | |
: V extends (c: XyzReadableRegistry<infer K2, infer U2>) => infer U // TODO: Generalize to more dependencies | |
? this extends XyzReadableRegistry<K2, U2> | |
? U | |
: never | |
: V | |
> | |
} | |
export interface XyzReadableRegistry<K extends string | symbol, V> { | |
get(key: K): V | |
} | |
class XyzDummyRegistry implements XyzWritableRegistry { | |
private items: Record<string | symbol, unknown> = {} | |
public register<K extends string | symbol, V>( | |
key: K, | |
value: V, | |
): asserts this is XyzReadableRegistry<K, V> { | |
this.items[key] = value | |
} | |
public get<K extends string | symbol>(key: K): unknown { | |
const v = this.items[key] | |
return typeof v === 'function' ? v(this) : v | |
} | |
} | |
export const createXyzRegistry = (): XyzWritableRegistry => | |
new XyzDummyRegistry() | |
// trying stuff | |
export const ccc: XyzWritableRegistry = createXyzRegistry() | |
class A {} | |
class B { | |
constructor(private readonly a: A) {} | |
f() { | |
console.log(this.a) | |
} | |
} | |
class C { | |
constructor(private readonly b: B) {} | |
f() { | |
console.log(this.b) | |
} | |
} | |
// If we try to add factories that depend on stuff that is still not registered, | |
// then the type checker will complain. | |
ccc.register('A', new A()) | |
ccc.register('B', (c: XyzReadableRegistry<'A', A>) => new B(c.get('A'))) | |
ccc.register('C', (c: XyzReadableRegistry<'B', B>) => new C(c.get('B'))) | |
const vvv = ccc.get('C') | |
console.log(vvv) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment