为 TypeScript 中的重载捕获传递函数的泛型类型

Capture generic type of passed function for overloads in TypeScript

我正在查看 the code in the streaming-iterables package, the pattern of defining an _fn and then overloading it with an fn including a curried version seems a case of DRY, happens in all functions 包裹公开。

下面的代码是我尝试对包进行一般柯里化重载实现:

type Leading<T extends any[]> = T extends [...infer I, infer _] ? I : never;
type Last<T extends any[]> = T extends [...infer _, infer I] ? I : never;

function curried<F extends (...args) => any>(
  fn: F
): {
  (...args: Parameters<F>): ReturnType<F>;
  (...args: Leading<Parameters<F>>): (curried: Last<Parameters<F>>) => ReturnType<F>;
} {
  return (...args) =>
    args.length == fn.length - 1
      ? curried => fn(...[...args, curried]) 
      : fn(...args);
}

generics 发挥作用之前,这似乎运作良好:

function a<T>(b: string, c: number, d: T[]) {
  return [b, c, ...d];
}

const f = curried(a);

f('hi', 42, [1, 2, 3]) // d: unknown[], expected: number[]
f('hi', 42) // curried: unknown[], expected: T[]

我显然知道 in TS at this time. But ?可以在此实施中采用这些解决方法吗?

我看不出有什么方法可以使用 来帮助您。该变通方法采用通用调用签名并将其转换为通用类型...但是您需要将一个通用调用签名转换为另一个通用调用签名,而这不会。并且在任何情况下,解决方法仅适用于单个通用调用签名,不适用于传递给 curried() 的任何可能签名。所以我不会再考虑这个了。


有一些 support for inferring generic functions but it only works in very specific circumstances and is thus fragile. Specifically it does not work when producing overloaded functions, such as you are doing with the result of curried(). See microsoft/TypeScript#33594 可以了解更多信息。如果您需要从 curried() 中重载,那么目前是不可能的。


如果我们放宽对重载的要求,只生成从函数末尾剥离一个参数的输出,您可以像这样编写 curried() 的调用签名:

declare function curried<I extends any[], L, R>(fn: (...args: [...I, L]) => R):
  ((...args: I) => (last: L) => R)

您可以像这样验证它是否有效:

function z<T>(a: T, b: T, c: T) { return [a, b, c]; }

const cZ = curried(z);
// const cZ: <T>(a: T, b: T) => (last: T) => T[]

const cz2 = cZ(5, 6);
// const cz2: (last: number) => number[]

通用输入,通用输出。 z 函数是泛型的,类型参数是 T,因此 cZ 也是泛型的,可以调用以指定 T。万岁!


请注意,我没有使用您的 a 功能进行演示。它仍然“有效”,但并不美妙。让我们试试吧:

function a<T>(b: string, c: number, d: T[]) { return [b, c, ...d]; }

const f = curried(a);
// const f: <T>(b: string, c: number) => (last: T[]) => (string | number | T)[]

const f2 = f('hi', 42);
// const f2: (last: unknown[]) => unknown[]

输出 f 是通用的,好吧。但是类型参数T没有好的推理站点。当你调用 f('hi', 42) 时,编译器需要立即推断出 T,但是 T 会是什么呢?它不依赖于 'hi'42。编译器不可能知道,所以它无论如何都会回到 unknown

解决这个问题的唯一方法是在调用 f 时自己指定 T,这使得它的通用性质有点不那么令人印象深刻:

const f3 = f<number>('hi', 42)
// const f3: (last: number[]) => (string | number)[]

可以说 curried(a) 输出的“正确”泛型类型应该是

declare const f: (b: string, c: number) => <T>(last: T[]) => (string | number | T)[]

我已将 T 从外部函数作用域移动到返回函数作用域。如果你有那个,那么你对 f() 的调用将产生另一个通用函数:

const f2 = f('hi', 42);
// const f2: <T>(last: T[]) => (string | number | T)[]

所以你不会再有 unknown:

const result = f2([1, 2, 3]);
// const result: (string | number)[]

但是编译器没有明显的方法知道它有时应该将泛型类型参数范围从输出函数移动到输出函数的输出函数。开发人员确实没有任何方法告诉编译器这样做,因为 TypeScript 缺乏谈论通用调用签名操作的表现力。

这将需要 更高种类的类型 microsoft/TypeScript#1213 or generic values of the sort requested in microsoft/TypeScript#17574... or maybe even existentially quantified generics of the sort requested in microsoft/TypeScript#14466 中请求的类型。或者他们三个都可以。或者是其他东西。不过,现在看来,它似乎遥不可及。


好了。如果你放宽你的要求并忍受一些边缘情况,你可以接近你想要的。如果你不能,那么我想我们可能不得不等到 TypeScript 的未来版本出现,支持更强大的泛型函数类型操作。

Playground link to code