I need something similar to a chain-of-responsibility pattern, where I can have a chain of processors and a state object, and as each processor is called, it can:
- Read the state object
- Add, modify, or remove fields from the state object
- Decide whether the request is completely satisfied and no further processors should be called, or if processing should continue with the next processor in the chain
I need these processors to be type-safe. The shape of the state object will change depending on the processors that are added to the chain.
To support the last requirement - controlling the execution flow (stop or continue after each processor is done), I wanted to use callbacks; if your processor wants processing to continue, it calls the callback; if your processor completed the processing and no other processors should run, it just does not call the callback. This is similar to middleware / request handlers in Express, for example.
But I'm having a problem when I try to reference the state object's type in defining the type that the callback accepts. If the processor makes no modifications to the type of the state object, everything works fine. But when I try to declare that a processor will modify the state object type, I lose the type on the state object for some reason. I'm sure there's something I'm not understanding about type inference.
Abbreviated version of that:
export type PipelineNext<TResult> = (result: TResult) => void;
export type PipelineHandler<T, TResult> = (state: T, next: PipelineNext<TResult>) => void;
export interface Pipeline<T> {
then<TOut = T>(fn: PipelineHandler<T, TOut>): Pipeline<TOut>;
end: () => void;
}
pipeline({ something: 'starting state' })
// The pipeline starts with an empty state object, which is passed into
// the first handler.
.then((state, next) => {
console.info(state.something);
next(state);
})
// And I can transform the state object into a completely different type
// just by defining the type on my PipelineNext.
.then((state, next: PipelineNext<{ original: string, name: string }>) => {
next({ original: state.something, name: 'foo' });
})
// And I get that state here, as expected
.then((state, next) => {
console.info(state.original);
console.info(state.name);
next(state);
})
// Here's where the problem starts ... let's say instead of redefining the
// response type, I want to simply amend it with something, and my processor
// doesn't know the type of the incoming state, so it can't redefine it.
// EXAMPLE 1: Use Omit to remove a field from the state object.
// PROBLEM: `state` becomes `any` here, which makes it a bit useless inside
// the handler.
.then((state, next: PipelineNext<Omit<typeof state, 'name'>>) => {
// Now state is `any`, which is useless inside this function
next({ original: state.original });
// But the callback is typed correctly (as demonstrated by
// uncommenting the next line)
// next({ foo: 'bar' });
})
It took me a while to even get the callback typing all correct, and the chaining correct, where each call to .then
gets the type from the callback. I feel like I'm real close, but just am not sure why the type inference is lost on state
when the processor is modifying that type, but works fine when there's no reference to the type.