Skip to content

Instantly share code, notes, and snippets.

@castarco
Last active February 9, 2022 13:13
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save castarco/f3d9723889d6529a5034c9dbd5dde841 to your computer and use it in GitHub Desktop.
Save castarco/f3d9723889d6529a5034c9dbd5dde841 to your computer and use it in GitHub Desktop.
A "new" pattern to create fully-typed registries/containers!
// 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 "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