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 ofOas "the current state object", thenCompose<O>is a generic function which takes athunkof type() => Tfor any other object typeT, and returns aT & O & {then: Compose<T & O>}... that is, the new object is the intersection ofTandO, and itsthen()method hasT & Oas 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