Skip to content

Instantly share code, notes, and snippets.

@hermanbanken
Last active March 18, 2022 15:01
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 hermanbanken/66d61fe5fc870f3011bc6f6c8cb4287c to your computer and use it in GitHub Desktop.
Save hermanbanken/66d61fe5fc870f3011bc6f6c8cb4287c to your computer and use it in GitHub Desktop.
Middleware

Sadly we can't have nice things in TypeScript (see https://www.ackee.agency/blog/typescript). Specifically middleware that:

  1. Notifies when the chain is broken
pipe(
   () => 21,
   (n: number) => n * 2,
   (n: string) => n.repeat(5), // Error: number is not assignable to string
);
  1. Infers the argument type in chain
pipe(
   () => 21,
   (n: number) => n * 2,
   n => { /* let n: number */ }, // (inferred from previous return)
);
  1. Have the correct type of the result
const meaning = pipe(
   () => 21,
   (n: number) => n * 2,
   (n: number) => 'Answer: ' + n,
);
// const meaning: () => string

The reason is that recurrent types are not possible:

type Chain<In, Out = In, Tmp = any> = [] // error: Type alias 'Chain' circularly references itself.ts(2456)
   | [(arg: In) => Out]
   | [
       (arg: In) => Tmp,
       ...Chain<Tmp, Out>
   ];
declare function flow<In, Out>(...fns: Chain<In, Out>): Out

The alternative is to either:

  1. construct different declarations up to a certain arity
  2. loose requirement 2

Unfortunately, requirement 2 is the very thing that we want to have, so we should resort to option 1.

// Sync example
declare type Middleware<input, output> = (r: input) => output;
declare function middleware<input, output>(): Middleware<input, output>;
function combineMiddleware<input, output, t>(a: Middleware<input, t>, b: Middleware<t, output>) {
return (r: input) => b(a(r));
}
// Async example
declare type AsyncMiddleware<input, output> = (r: input) => output | PromiseLike<output>;
declare function middleware<input, output>(): AsyncMiddleware<input, output>;
function combineAsyncMiddleware<input, output, t>(
a: AsyncMiddleware<input, t>,
b: AsyncMiddleware<t, output>,
): AsyncMiddleware<input, output> {
return (r: input) => {
const intermediate = a(r);
return isPromise<t>(intermediate) ? intermediate.then(v => b(v)) : b(intermediate);
};
}
function isPromise<T>(v: any): v is PromiseLike<T> {
return 'then' in v && typeof v.then === 'function';
}
// But async is just another middleware type; so you can also stack sync middlewares
function combineWithAsyncMiddleware<input, output, t>(
a: Middleware<input, t> | AsyncMiddleware<input, t>,
b: Middleware<Awaited<t>, output>,
): AsyncMiddleware<input, output> {
return (r: input) => {
const intermediate = a(r);
return (isPromise<t>(intermediate) ? intermediate : Promise.resolve(intermediate)).then(async v => b(await v));
};
}
// Some middlewares run stuff before+after
function wrappingMiddlewareExample<input, output>(delegate: Middleware<input, output>): Middleware<input, output> {
return r => {
const start = Date.now();
try {
return delegate(r);
} finally {
console.log('duration', Date.now() - start);
}
};
}
interface IBuilder<input, output> {
/**
* Regular middleware chaining
*/
next<t>(fn: Middleware<output, t>): IBuilder<input, t>;
/**
* If our middleware returns a Promise type, this nextAsync
* will await the intermediate value before passing it onto the next middleware
*/
nextAsync<t>(fn: Middleware<Awaited<output>, t>): IBuilder<input, t | PromiseLike<t>>;
/**
* Wraps function around the current middleware.
* Its function will run before & after the current middleware.
*/
wrap(fn: (hole: (t: input) => output) => (t: input) => output): IBuilder<input, output>;
}
// Builder pattern
class Builder<input, output> implements IBuilder<input, output> {
constructor(private middleware: Middleware<input, output>) {}
/**
* Regular middleware chaining
*/
public next<t>(fn: Middleware<output, t>): Builder<input, t> {
return new Builder(combineMiddleware(this.middleware, fn));
}
/**
* If our middleware returns a Promise type, this nextAsync
* will await the intermediate value before passing it onto the next middleware
*/
public nextAsync<t>(fn: Middleware<Awaited<output>, t>): Builder<input, t | PromiseLike<t>> {
return new Builder(combineWithAsyncMiddleware(this.middleware, fn));
}
/**
* Wraps function around the current middleware.
* Its function will run before & after the current middleware.
*/
public wrap(fn: (hole: (t: input) => output) => (t: input) => output): Builder<input, output> {
return new Builder(fn(this.middleware));
}
public build(): Middleware<input, output> {
return this.middleware;
}
}
type IChainable<input, output> = {
/**
* Regular middleware chaining
*/
next<t>(fn: Middleware<output, t>): IChainable<input, t>;
/**
* If our middleware returns a Promise type, this nextAsync
* will await the intermediate value before passing it onto the next middleware
*/
nextAsync<t>(fn: Middleware<Awaited<output>, t>): IChainable<input, t | PromiseLike<t>>;
/**
* Wraps function around the current middleware.
* Its function will run before & after the current middleware.
*/
wrap(fn: (hole: (t: input) => output) => (t: input) => output): IChainable<input, output>;
/**
* Instantiates the chainable to a real function
* @param fn
*/
// build(fn: Middleware<input, output>): Middleware<input, output>;
};
// Builder pattern
class Chainable<input, output, A = input, B = output> implements IChainable<input, output> {
constructor(private middleware: (m: Middleware<A, B>) => Middleware<input, output>) {}
/**
* Regular middleware chaining
*/
public next<t>(fn: Middleware<output, t>) {
return new Chainable<input, t, A, B>((inside: Middleware<A, B>) => combineMiddleware(this.middleware(inside), fn));
}
/**
* If our middleware returns a Promise type, this nextAsync
* will await the intermediate value before passing it onto the next middleware
*/
public nextAsync<t>(fn: Middleware<Awaited<output>, t>) {
return new Chainable<input, t | PromiseLike<t>, A, B>((inside: Middleware<A, B>) =>
combineWithAsyncMiddleware(this.middleware(inside), fn),
);
}
/**
* Wraps function around the current middleware.
* Its function will run before & after the current middleware.
*/
public wrap(fn: (hole: (t: input) => output) => (t: input) => output) {
return new Chainable<input, output, A, B>(inside => fn(this.middleware(inside)));
}
static builder<input, output>(): (fn: Middleware<input, output>) => Chainable<input, output> {
return fn => new Chainable<input, output>(() => fn);
}
}
// Builder pattern
class Chainable2<input, output> {
/**
* Regular middleware chaining
*/
public next<t>(fn: Middleware<output, t>): (inner: Middleware<input, output>) => Builder<input, t> {
return inner => new Builder(combineMiddleware(inner, fn));
}
/**
* If our middleware returns a Promise type, this nextAsync
* will await the intermediate value before passing it onto the next middleware
*/
public nextAsync<t>(
fn: Middleware<Awaited<output>, t>,
): (inner: Middleware<input, output>) => Builder<input, t | PromiseLike<t>> {
return inner => new Builder(combineWithAsyncMiddleware(inner, fn));
}
/**
* Wraps function around the current middleware.
* Its function will run before & after the current middleware.
*/
public wrap(
fn: (hole: (t: input) => output) => (t: input) => output,
): (inner: Middleware<input, output>) => Builder<input, output> {
return inner => new Builder(fn(inner));
}
public build(): Middleware<input, output> {
return this.middleware;
}
}
// Real world examples
type Bookstore = Function;
type InitialReqType = { locals: {} };
type FullReqType = { locals: { bookstore: Bookstore } };
type FullRespType = {};
const withTiming = fn => {
return a => {
const start = Date.now();
try {
return fn(a);
} finally {
console.log('duration', Date.now() - start);
}
};
};
function withBookstore<T extends InitialReqType>(): Middleware<
T,
T & { locals: T['locals'] & { bookstore: Bookstore } }
> {
return req => ({ ...req, locals: { ...req.locals, bookstore: () => 'book' } });
}
const configBuilder = <A extends FullReqType, B>(handler: Middleware<A, B>): Middleware<FullReqType, FullRespType> =>
new Builder<A, B>(handler)
// .next(withBookstore)
// .wrap(withTiming)
.build();
type Req<T> = { locals: T };
type LocalsMiddleware<B, A> = Middleware<Req<B>, Req<A>>;
configBuilder(() => {
return {};
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment