TypeScript 中的函数链工厂保留 input/output 类型?

Function chain factory in TypeScript preserving input/output types?

考虑这个金字塔代码:


/**
 * @template T, U
 * @param {T} data
 * @param {(data: T) => Promise<U>} fn
 */
function makeNexter(data, fn) {
  return {
    data,
    next: async () => fn(data),
  };
}

return makeNexter({}, async (data) => {

  return makeNexter({ ...data, a: 3 }, async (data) => {

    return makeNexter({ ...data, b: 'hi' }, async (data) => {

    });

  });

});

有没有办法制作一个函数,在数组中接受不定数量的这些函数,并按顺序将每个函数的结果应用于下一个函数,同时保留类型信息,以便每个数据参数在内部函数具有正确的推断类型?

基本上我试图使这段代码扁平化而不是金字塔形,同时在每个参数中保留类型信息。

另一种看待这个问题的方法是,我正在尝试制作一种生成器函数,其生成的类型在每一步都不需要是 asserted/filtered(在函数生成器的类型联合之外通常 return),但在每一步都明确知道,因为它们总是线性传递。

换句话说,是否可以使用正确的类型信息在 TypeScript 中创建此函数?


/** @type {???} */
return makeNexters({}, [
  async (data) => {
    return { ...data, a: 3 };
  },
  async (data) => {
    return { ...data, b: 'hi' };
  },
  async (data) => {
    // data here should have type:
    //   {
    //     b: string;
    //     a: number;
    //   }
  },
]);

另请参阅我在 TypeScript 问题中的功能请求:https://github.com/microsoft/TypeScript/issues/43150

我怀疑您能否想出一个解决方案,在该解决方案中,编译器可以按照您想要的方式推断类型,而无需在调用函数时进行任何额外工作。

TypeScript 存在设计限制,在 microsoft/TypeScript#38872 中突出显示,当这些推断需要相互依赖时,编译器无法同时推断泛型类型参数和回调参数的上下文类型。当你打电话时:

return makeNexters({}, [
  async (data) => { return { ...data, a: 3 };  },
  async (data) => { return { ...data, b: 'hi' }; },
  async (data) => {},
])

你大概是在要求编译器使用 {} 来推断一些泛型类型参数(或其中的一部分),这将用于推断第一个 data 回调参数的类型,然后将用于推断一些泛型类型参数(或其中的一部分),然后将用于推断第二个 data 回调参数的类型,等等。但是编译器只执行有限数量的类型推断阶段,并且可能在推断出其中的第一个或两个之后就放弃。


从概念上讲,我会将 makeNexters() 的类型表示为如下所示:

type Idx<T, K> = K extends keyof T ? T[K] : never

declare function makeNexters<T, R extends readonly any[]>(
  init: T, next: readonly [...{ [K in keyof R]: (data: Idx<[T, ...R], K>) => Promise<R[K]> }]
): void;

我说的是 init 参数是泛型类型 T,而 next 参数是 maps over a tuple type R. next 中的每个元素都应该是一个接受 R 中“前一个”元素的 data 参数的函数(除了第一个从 T 接受它的参数) ,并且 return 将 Promise 添加到 R 中的“当前”元素。

(我什至不担心这个东西 return 是 void 的事实。如果推理有效,那么我会担心用术语表达 return 类型TR,但现在这不是重点)

并且此函数 起作用,但不是以您想要的方式推断。您基本上可以放弃回调参数的上下文推断并获得非常好的泛型类型推断:

makeNexters({}, [
  async (data: {}) => { return { ...data, a: 3 }; },
  async (data: { a: number }) => { return { ...data, b: 'hi' }; },
  async (data: { a: number, b: string }) => { },
]);
/*function makeNexters<{}, [{ a: number; }, { b: string; a: number; }, void]>() */

或者您可以放弃泛型类型推断并为回调参数获得非常好的上下文类型推断:

makeNexters<{}, [{ a: number }, { b: string, a: number }, void]>({}, [
  async (data) => { return { ...data, a: 3 }; },
  async (data) => { return { ...data, b: 'hi' }; },
  async (data) => { }
]);

如果你试图让编译器两者都不给你:它会在整个地方推断出 any

makeNexters({}, [
  async (data) => { return { ...data, a: 3 }; },
  async (data) => { return { ...data, b: 'hi' }; },
  async (data) => { }
]);
/* function makeNexters<{}, [any, any, void]>*/

并且可能任何必须写出类型 {b: string, a: number} 的代码都违背了此链接函数的目的。


在完全放弃之前,我建议您考虑将您的方法更改为通过创建一个函数 return 另一个函数来工作的方法。从本质上讲,这是 builder 的一种形式,不是一次创建“下一个”,链由单个数组表示,而是分阶段创建,链中的每个 link 是由函数调用伪造。这些调用没有嵌套,所以它不再是“金字塔”了。你会像这样使用它:

const p = makeNexterChain({})
  .and(async data => ({ ...data, a: 3 }))
  .and(async data => ({ ...data, b: "hi" }))
  .done

并且生成的 p 类型为

/*const p: {
    data: {};
    next: () => Promise<{
        data: {
            a: number;
        };
        next: () => Promise<{
            data: {
                b: string;
                a: number;
            };
            next: () => Promise<void>;
        }>;
    }>;
} */

这与原始嵌套版本相同。

makeNexterChain() 的实现超出了这个问题的范围,因为您似乎在询问类型而不是运行时行为。您大概可以通过适当使用各种承诺的 then() 方法来实现 makeNexterChain()

无论如何,这是 makeNexterChain() 的输入:

type Nexter<R extends any[]> = R extends [infer H, ...infer T] ? {
  data: H
  next: () => Promise<Nexter<T>>
} : void

type Last<T> = T extends [...infer F, infer L] ? L : never

interface NexterChain<R extends any[]> {
  and<U>(cb: (data: Last<R>) => Promise<U>): NexterChain<[...R, U]>
  done: Nexter<R>
}
declare function makeNexterChain<T>(init: T): NexterChain<[T]>;

本质上,您希望 makeNexterChain() 获取类型为 T 的初始元素,并且 return 为 NexterChain<[T]>。每个 NexterChain<R>(其中 R 是元组类型)都有一个 and() 方法将新类型附加到 R 的末尾,以及一个 done() 方法return 一个 Nexter<R>Nexter<R> 有一个 data 属性 ,其类型是 R 的第一个元素,以及一个没有参数的 next() 方法,它产生一个 Promise 的新 Nexter<T>,其中 TR 相同,但删除了第一个元素。哦,当它结束时你会得到 void.

你可以在这里看到类型推断确实非常有效。在每一步,编译器都知道 data 回调参数是什么,您不需要手动指定任何通用参数或手动注释任何回调参数。希望您可以使用像这样的解决方案,而不是反对 TypeScript 的类型推断功能。

Playground link to code