TypeScript 中由泛型类型组成的对象(类型递归)

Objects composed of generic types in TypeScript (type recursion)

给出以下未类型化的 TS:

const compose = (thunk: any): any => {
  const res = { ...thunk() };
  return { ...res, then: (f: any): any => compose(() => ({...res, ...f()})) };
};

我们可以用它来制作可组合的对象:

const { foo, bar } = compose(() => ({foo: 1})).then(() => ({bar: 2}))
// foo: 1, bar: 2

但是在 TS 中输入这个似乎很棘手,因为类型是递归的。

我能想到的最好的是:

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() })) };
};

这意味着落在 compose2 之外的所有对象都是 any 类型的。 我想要完成的最终结果是使用所有组合对象键入的内容:

const combined = compose(() => ({foo: 1})).then(() => ({bar: 2}))
// type of combined: { foo: number } & { bar: number } & { then: … }

在我看来,我们需要某种可以打上递归结的 Fix 类型,因为 Composethen 的类型需要递归。

当然,如果我们反转compose的签名并以某种方式使用CPS,也许有办法做到这一点。我愿意接受建议!

请注意,2 进制 compose,我们称之为 combine,没有问题:

const combine = <A extends {}, B extends {}>(a: () => A, b: () => B): A & B => ({...a(), ...b()});
const bar = combine(() => ({foo: 1}), () => combine(() => ({bar: 2}), () => ({baz: 3})) )

但是编写语句并不是特别好,所以我希望从结果对象传递一个闭包,这样我就不必嵌套重复的函数调用。

我想你可能正在寻找这个:

type Compose<O extends object = {}> =
    <T extends object>(thunk: () => T) => T & O & {
        then: Compose<T & O>
    }

const compose: Compose = (thunk) => {
    const res = { ...thunk() };
    return { ...res, then: f => compose(() => ({ ...res, ...f() })) };
};

then() 的 return 类型带有一些您需要在您的组合类型中表示的状态信息。如果我们将 O 视为“当前状态对象”,那么 Compose<O> 是一个通用函数,它接受类型为 () => Tthunk 用于任何其他对象类型 T,而return是一个T & O & {then: Compose<T & O>}...即新对象是TO的交集,其then()方法有T & O作为新状态。

compose() 类型检查的实施是一个好兆头。让我们验证编译器是否理解调用的工作原理:

const { foo, bar } = compose(() => ({ foo: 1 })).then(() => ({ bar: "hello" }));

console.log(foo.toFixed(2)) // 1.00
console.log(bar.toUpperCase()); // HELLO

看起来不错!

Playground link to code