在TypeScript中为重载函数捕获传递函数的通用类型

4
我正在查看流式可迭代对象包中的代码,定义_fn并使用一个包含柯里化版本的fn进行重载的模式似乎是DRY原则的一种体现,在该包中所有函数都会出现这种情况。
下面的代码是我尝试为该包实现通用柯里化重载的代码:
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);
}

这种方法在涉及到 泛型 时似乎效果不佳:
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[]

我知道目前 TS 对高阶泛型的支持有所欠缺。但是似乎有变通方法?这些变通方法能否在此实现中采用?

1个回答

4

我没有看到任何使用指定通用函数的解决方法来帮助你。该解决方法将通用调用签名转换为通用类型......但你需要将一个通用调用签名转换为另一个通用调用签名,这个方法无法实现。而且无论如何,此解决方法仅适用于单个通用调用签名,而不适用于传递给curried()的任何可能签名。因此,我将停止考虑此方法。


有一些支持推断通用函数的功能,但只在非常特定的情况下才能运行,并且因此很脆弱。具体来说,在生成重载函数时不起作用,例如你正在使用curried()的结果。更多信息请参见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)[]

因此,您将不再拥有未知

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

但编译器很难知道有时应该将通用类型参数范围从输出函数移动到输出函数的输出函数中。由于 TypeScript 缺乏对泛型调用签名操作的表达能力,开发者实际上也没有办法告诉编译器这样做。

要做到这一点,需要更高级的类型(higher kinded types),比如在microsoft/TypeScript#1213 中提出的那种,或者更通用的值(generic values),比如在microsoft/TypeScript#17574 中提出的那种……甚至可能是在microsoft/TypeScript#14466 中提出的存在量化泛型(existentially quantified generics)。也可能是以上三种方案结合使用,或者采用其它方案。尽管现在看来似乎还无法实现。


所以说,如果您可以放宽要求并容忍一些边缘情况,那么就可以接近您想要的结果。如果不能容忍,那么我想我们可能要等到 TypeScript 的某个未来版本支持更强大的泛型函数类型操作。

代码演示链接


你的实现很牛 @jcalz! 非常有见地,值得赞扬。可惜,如果用户需要指定通用类型,那么上游软件包的要求将无法满足。猜想我们只能等待了! - Keyvan
很遗憾编译器没有像你指出的那样推断“正确的泛型类型”。我明白它为什么不能这样做。 - Keyvan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接