Given the following untyped TS:
const compose = (thunk: any): any => {
const res = { ...thunk() };
return { ...res, then: (f: any): any => compose(() => ({...res, ...f()})) };
};
We can use it to make composable objects:
const { foo, bar } = compose(() => ({foo: 1})).then(() => ({bar: 2}))
// foo: 1, bar: 2
But typing this in TS seems tricky, as the types are recursive.
The best I could come up with is:
type Compose<T> = (thunk: () => T) => T & { then: Compose<any> };
const compose2 = <T extends {}>(thunk: () => T): ReturnType<Compose<T>> => {
const res = { ...thunk() };
return { ...res, then: (f) => compose2(() => ({ ...res, ...f() })) };
};
This means that all the objects that fall out of compose2 are of type any.
The end result I'd like to accomplish is something that's typed with all of the composed objects:
const combined = compose(() => ({foo: 1})).then(() => ({bar: 2}))
// type of combined: { foo: number } & { bar: number } & { then: … }
The way I see it we'd need some sort of Fix type that can tie the recursive knot, as the type for then in Compose needs to recurse.
Of course, there might be a way to do it if we inverted the signature of compose and somehow used CPS. I'm open for suggestions!
Note that a 2-ary compose, let's call it combine, is no issue:
const combine = <A extends {}, B extends {}>(a: () => A, b: () => B): A & B => ({...a(), ...b()});
const bar = combine(() => ({foo: 1}), () => combine(() => ({bar: 2}), () => ({baz: 3})) )
But it isn't particularly nice to write the statements, so I hoped to pass a closure from the resulting object so I wouldn't have to nest repeated function calls.