Skip to content

Instantly share code, notes, and snippets.

@mindplay-dk
Last active July 19, 2023 16:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mindplay-dk/64e1f07028c1749e06c4fc2fa8cea785 to your computer and use it in GitHub Desktop.
Save mindplay-dk/64e1f07028c1749e06c4fc2fa8cea785 to your computer and use it in GitHub Desktop.
Minimal DI Container in TypeScript
type Container<TServiceMap> = <Name extends keyof TServiceMap>(name: Name) => Promise<TServiceMap[Name]>;
type Factory<TServiceMap, Name extends keyof TServiceMap> = (container: Container<TServiceMap>) => Promise<TServiceMap[Name]>;
type FactoryMap<TServiceMap> = { [Name in keyof TServiceMap]: Factory<TServiceMap, Name> };
function Container<TServiceMap>(map: FactoryMap<TServiceMap>): Container<TServiceMap> {
const resolved: { [Name in keyof TServiceMap]?: Promise<TServiceMap[Name]> } = {};
return function resolve<Name extends keyof TServiceMap>(name: Name): Promise<TServiceMap[Name]> {
return resolved[name]! || (resolved[name] = map[name](resolve));
}
}
// EXAMPLE:
interface ServiceMap {
UserCache: UserCache;
UserService: UserService;
}
class UserCache { }
class UserService {
constructor(private cache: UserCache) { }
}
let container = Container<ServiceMap>({
UserCache: async $ => new UserCache(),
UserService: async $ => new UserService(await $("UserCache"))
});
container("UserService").then(service => {
console.log(service);
container("UserService").then(same_service => {
console.log(service === same_service); // true
});
});
type Container<TServiceMap> = <Name extends keyof TServiceMap>(name: Name) => TServiceMap[Name];
type Factory<TServiceMap, Name extends keyof TServiceMap> = (container: Container<TServiceMap>) => TServiceMap[Name];
type FactoryMap<TServiceMap> = { [Name in keyof TServiceMap]: Factory<TServiceMap, Name> };
function Container<TServiceMap>(map: FactoryMap<TServiceMap>): Container<TServiceMap> {
const resolved: { [Name in keyof TServiceMap]?: TServiceMap[Name] } = {};
return function resolve<Name extends keyof TServiceMap>(name: Name): TServiceMap[Name] {
return resolved[name]! || (resolved[name] = map[name](resolve));
}
}
// EXAMPLE:
interface ServiceMap {
UserCache: UserCache;
UserService: UserService;
}
class UserCache { }
class UserService {
constructor(private cache: UserCache) { }
}
let container = Container<ServiceMap>({
UserCache: $ => new UserCache(),
UserService: $ => new UserService($("UserCache"))
});
console.log(container("UserService"));
console.log(container("UserService") === container("UserService")); // true
// FRAMEWORK:
type Container<T> = {
[P in keyof T]: T[P] extends () => unknown ? ReturnType<T[P]> : never
}
type Factory<T> = T & ThisType<Container<T>>
function createContainer<T>(factory: Factory<T>): Container<T> {
const container = {} as Container<T>;
const instances = {} as { [key: string]: any };
const has = Object.prototype.hasOwnProperty.bind(instances);
for (const key in factory) {
Object.defineProperty(
container,
key,
{
get() {
if (! has(key)) {
instances[key] = (factory[key] as any).apply(container);
}
return instances[key];
},
configurable: true,
enumerable: true,
}
);
}
return container;
}
function createProvider<T>(factory: Factory<T>): typeof factory {
return factory;
}
// USE CASE:
type App = Container<typeof appProvider>;
type Messaging = Container<typeof messagingProvider>;
function showMessage(window: Messaging["window"], message: string) {
window.alert(message);
}
function showWelcomeMessage(showMessage: Messaging["showMessage"], user: string) {
showMessage(`Welcome, ${user}`);
}
function login(showWelcomeMessage: App["showWelcomeMessage"], user: string) {
showWelcomeMessage(user);
}
// EXAMPLE:
const messagingProvider = createProvider({
window() {
return window;
},
showMessage() {
return showMessage.bind(null, this.window);
},
});
const appProvider = createProvider({
...messagingProvider,
showWelcomeMessage() {
return showWelcomeMessage.bind(null, this.showMessage);
},
login() {
return login.bind(null, this.showWelcomeMessage);
}
});
const container = createContainer(appProvider);
container.login("Bob");
// FRAMEWORK:
type ReturnTypeMap<T> = {
[P in keyof T]: T[P] extends () => unknown ? ReturnType<T[P]> : never
}
type ContainerType<T> = ThisType<ReturnTypeMap<T>>
type FactoryMap<T> = T & ContainerType<T>
function createContainer<T>(factory: FactoryMap<T>): ReturnTypeMap<T> {
const container = {} as ReturnTypeMap<T>;
const instances = {} as { [key: string]: any };
const has = Object.prototype.hasOwnProperty.bind(instances);
for (const key in factory) {
Object.defineProperty(
container,
key,
{
get() {
if (! has(key)) {
instances[key] = (factory[key] as any).apply(container);
}
return instances[key];
},
configurable: true,
enumerable: true,
}
);
}
return container;
}
// USE CASE:
const $showMessage = (window: Window) => (message: string) => {
window.alert(message);
}
type showMessage = ReturnType<typeof $showMessage>;
const $showWelcomeMessage = (showMessage: showMessage) => (user: string) => {
showMessage(`Welcome, ${user}`);
}
type showWelcomeMessage = ReturnType<typeof $showWelcomeMessage>;
const $login = (showWelcomeMessage: showWelcomeMessage) => (user: string) => {
showWelcomeMessage(user);
}
type login = ReturnType<typeof $login>;
// EXAMPLE:
const container = createContainer({
window() {
return window;
},
showMessage() {
return $showMessage(this.window);
},
showWelcomeMessage() {
return $showWelcomeMessage(this.showMessage);
},
login() {
return $login(this.showWelcomeMessage);
}
});
container.login("Bob");
/*
// ----- alternative, container-first approach:
// USE CASE:
type App = typeof container;
const $showMessage = (window: App["window"]) => (message: string) => {
window.alert(message);
}
const $showWelcomeMessage = (showMessage: App["showMessage"]) => (user: string) => {
showMessage(`Welcome, ${user}`);
}
const $login = (showWelcomeMessage: App["showWelcomeMessage"]) => (user: string) => {
showWelcomeMessage(user);
}
// EXAMPLE:
const container = createContainer({
window() {
return window;
},
showMessage() {
return $showMessage(this.window);
},
showWelcomeMessage() {
return $showWelcomeMessage(this.showMessage);
},
login() {
return $login(this.showWelcomeMessage);
}
});
container.login("Bob");
*/
/*
// ----- alternative, container-first, flat functions approach:
// USE CASE:
type App = typeof container;
function showMessage(window: App["window"], message: string) {
window.alert(message);
}
function showWelcomeMessage(showMessage: App["showMessage"], user: string) {
showMessage(`Welcome, ${user}`);
}
function login(showWelcomeMessage: App["showWelcomeMessage"], user: string) {
showWelcomeMessage(user);
}
// EXAMPLE:
const container = createContainer({
window() {
return window;
},
showMessage() {
return showMessage.bind(null, this.window);
},
showWelcomeMessage() {
return showWelcomeMessage.bind(null, this.showMessage);
},
login() {
return login.bind(null, this.showWelcomeMessage);
}
});
container.login("Bob");
*/
/*
// ----- alternative, container-first, flat functions approach with modularity:
// USE CASE:
const $showMessage = (window: System["window"]) => (message: string) => {
window.alert(message);
}
const $showWelcomeMessage = (showMessage: System["showMessage"]) => (user: string) => {
showMessage(`Welcome, ${user}`);
}
const $login = (showWelcomeMessage: App["showWelcomeMessage"]) => (user: string) => {
showWelcomeMessage(user);
}
// EXAMPLE:
function createAppContainer(system = createSystemContainer(window)) {
return createContainer({
showWelcomeMessage() {
return $showWelcomeMessage(system.showMessage);
},
login() {
return $login(this.showWelcomeMessage);
}
});
}
type App = ReturnType<typeof createAppContainer>;
function createSystemContainer(window: Window) {
return createContainer({
window() {
return window;
},
showMessage() {
return $showMessage(this.window);
},
});
}
type System = ReturnType<typeof createSystemContainer>;
const app = createAppContainer();
app.login("Bob");
*/
// FRAMEWORK:
type Async<T> = {
[K in keyof T as `$${string & K}`]: Promise<T[K]>;
}
type FactoryMap<T> = {
[K in keyof T]: (f: Omit<T, K>) => T[K];
}
function createContainer<T>(factory: FactoryMap<T>): T {
const container = {} as T;
const instances = {} as { [key: string]: any };
const has = Object.prototype.hasOwnProperty.bind(instances);
for (const key in factory) {
Object.defineProperty(
container,
key,
{
get() {
if (! has(key)) {
instances[key] = factory[key](container);
}
return instances[key];
},
configurable: true,
enumerable: true,
}
);
}
return container;
}
// CASE STUDY:
class UserCache {
public getUser() {
return "bob";
}
}
class UserService {
constructor(public cache: UserCache) { }
}
interface UserServiceMap {
userCache: UserCache;
userService: UserService;
}
// CONTAINER EXAMPLE:
{
const container = createContainer<UserServiceMap>({
userCache: () => new UserCache(),
userService: ({ userCache }) => new UserService(userCache),
});
console.log(container.userService.cache === container.userCache);
console.log(container.userCache === container.userCache);
}
// ASYNC CONTAINER EXAMPLE:
(async () => {
const container = createContainer<Async<UserServiceMap>>({
$userCache: async () => new UserCache(),
$userService: async ({ $userCache }) => new UserService(await $userCache),
});
console.log((await container.$userService).cache === await container.$userCache);
console.log((await container.$userCache) === await container.$userCache);
})();
/*
// alternative, specification-first approach:
// USE CASE:
interface S {
window: Window;
login(user: string): void;
showMessage(message: string): void;
showWelcomeMessage(message: string): void;
}
const $showMessage = (window: S["window"]): S["showMessage"] => (message) => {
window.alert(message);
}
const $showWelcomeMessage = (showMessage: S["showMessage"]): S["showWelcomeMessage"] => (user) => {
showMessage(`Welcome, ${user}`);
}
const $login = (showWelcomeMessage: S["showWelcomeMessage"]): S["login"] => (user) => {
showWelcomeMessage(user);
}
// EXAMPLE:
const container = createContainer<S>({
window: () => window,
showMessage: ({ window }) => $showMessage(window),
showWelcomeMessage: ({ showMessage }) => $showWelcomeMessage(showMessage),
login: ({ showWelcomeMessage }) => $login(showWelcomeMessage),
});
container.login("Bob");
*/
// DEPENDENCIES:
class SharedCache { }
class UserService {
constructor(public cache: SharedCache) { }
}
class ProductService {
constructor(public cache: SharedCache) { }
}
class App {
constructor(public userService: UserService, public productService: ProductService) {}
}
// EXAMPLE:
function one<T>(make: () => T): () => T {
let instance: T;
return () => instance || (instance = make());
}
function bootstrap() {
const sharedCache = one(() => new SharedCache());
const userService = one(() => new UserService(sharedCache()));
const productService = one(() => new ProductService(sharedCache()));
const app = one(() => new App(userService(), productService()));
return app();
}
const app = bootstrap();
console.log(app.userService.cache === app.productService.cache); // TRUE
@mindplay-dk
Copy link
Author

I wrote a longer article about the IOC pattern I settled on:

https://dev.to/mindplay/a-successful-ioc-pattern-with-functions-in-typescript-2nac

@mindplay-dk
Copy link
Author

May want to explore something like this in TypeScript as well. 🤔

@mindplay-dk
Copy link
Author

Added another example using this for context - it looks like the built-in ThisType<T> (an empty marker interface according to the manual and source-code) uses some kind of built-in compiler magic, which is somehow able to break the "no cicular types" limitation. (??)

@mindplay-dk
Copy link
Author

Added another approach using this with multiple providers. This seems promising, but I'm still exploring other modular patterns...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment