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.
I think you might be looking for this:
The return type of
then()
carries some state information you need to represent in your compose type. If we think ofO
as "the current state object", thenCompose<O>
is a generic function which takes athunk
of type() => T
for any other object typeT
, and returns aT & O & {then: Compose<T & O>}
... that is, the new object is the intersection ofT
andO
, and itsthen()
method hasT & O
as the new state.The fact that the implementation of
compose()
type checks is a good sign. Let's verify that the compiler understands how calls work:Looks good!
Playground link to code