TypeScript 中的函数链工厂保留输入/输出类型



考虑这个金字塔代码:


/**
* @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) => {
});
});
});

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

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

另一种看待这个问题的方法是,我正在尝试制作一种生成器函数,其生成的类型不需要在每一步都断言/过滤(从函数生成器通常返回的类型联合中),而是在每一步都明确已知,因为它们总是线性传递的。

换句话说,是否可以使用正确的类型信息在 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参数是映射到元组类型R的类型。next中的每个元素都应该是一个函数,它接受R中"上一个"元素的data参数(除了第一个从T接受它),并返回对R中"当前"元素的Promise

(我什至不担心这个东西会返回void.如果推理有效,那么我会担心用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}的代码都违背了此链接函数的目的。


在完全放弃之前,我建议你考虑通过创建一个返回另一个函数的函数来改变你的方法。 从本质上讲,这是一种构建器形式,其中不是一次创建"nexter",其中链由单个数组表示,而是分阶段进行,其中链中的每个链接都由函数调用锻造。这些调用不是嵌套的,因此它不再是"金字塔"。 你可以像这样使用它:

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的初始元素,并返回一个NexterChain<[T]>。 每个NexterChain<R>(其中R是元组类型)都有一个and()方法,用于将新类型附加到R的末尾,以及一个返回Nexter<R>done()方法。 一个Nexter<R>有一个data属性,其类型是R的第一个元素,以及一个没有参数的next()方法,它产生一个新Nexter<T>Promise,其中T与删除第一个元素的R相同。 哦,当它到达最后时,你会得到void.

你可以看到,在这里,类型推断确实非常有效。 在每个步骤中,编译器都知道data回调参数是什么,并且您无需手动指定任何泛型参数或手动批注任何回调参数。 希望您可以使用这样的解决方案,它适用于而不是反对TypeScript的类型推断功能。

操场链接到代码

最新更新