如何修复递归函数的“并非所有类型的成分都是可调用的”?

How can I fix "Not all constituents of type are callable" for a recursive function?

提问人:Homo Civicus 提问时间:7/31/2023 最后编辑:Homo Civicus 更新时间:8/9/2023 访问量:64

问:

所以我有一个函数来讨好其他函数:

const curry = <TArg, TReturn>(fn: (...args: TArg[]) => TReturn) => {
  const curried = (...args: TArg[]) =>
    args.length < fn.length
      ? (...innerArgs: TArg[]) => curried(...args, ...innerArgs)
      : fn(...args);
  return curried;
};

const join = (a: number, b: number, c: number) => {
  return `${a}_${b}_${c}`;
};

const curriedJoin = curry(join);

curriedJoin(1)(2, 3); // Should return '1_2_3', but gives an error

Typescript 不允许我多次调用它,因为第一次调用可能返回了一个字符串。你会如何解决它?

JavaScript TypeScript 算法 TypeScript-Generics 咖喱

评论

0赞 jcalz 7/31/2023
这种方法是否满足您的需求?你无法真正获得编译器验证的实现安全性,因为编译器永远不会“获得”你必须做的参数列表的剥离来表达这一点。所以这是我们能得到的最接近的。如果它满足您的需求,我会写一个答案来解释;如果没有,我错过了什么?
0赞 Homo Civicus 8/8/2023
@jcalz,看起来这是我们能得到的最好的方法

答:

0赞 jcalz 8/9/2023 #1

唯一可行的方法是返回一个函数,其输出类型在很大程度上取决于输入函数中的参数数和传入的参数数。这意味着您不仅需要它是泛型的,还需要使用条件类型来表示差异。你需要该函数的类型是递归的,所以我们必须给它起一个名字:curry()

interface Curried<A extends any[], R> {
  <AA extends Partial<A>>(...args: AA): 
    A extends [...{ [I in keyof AA]: any }, ...infer AR] ?
      [] extends AR ? R : Curried<AR, R> : never;
}

因此,a 表示函数的 curried 版本,其参数列表是 ,其返回类型是 。您可以使用一些通用的 rest 参数类型来调用它,该参数必须可分配给 (使用 Partial<T> 实用程序类型)。然后我们需要找出参数列表的其余部分,它被推断为红色(请注意,这也许会起作用,但如果由于某种原因比 的初始部分窄,那将失败。所以只是意味着“任何与”相同长度的东西“)。如果为空 (),则 curried 函数返回 。否则,它将返回 ,以便也可以调用生成的函数。Curried<A, R>ARargsAAAARA extends [...AA, ...infer AR] ? ⋯AAA{[I in keyof AA]: any}AAAR[]RCurried<AR, R>

实现可能如下所示:

const curry = <A extends any[], R>(fn: (...args: A) => R) => {
  const curried = (...args: any): any =>
    args.length < fn.length
      ? (...innerArgs: A[]) => curried(...args, ...innerArgs)
      : fn(...args);
  return curried as Curried<A, R>;
};

请注意,我使用了 any 类型和类型断言来说服编译器 的实现是可以接受的。编译器无法真正理解许多高阶泛型类型操作,也无法验证哪些函数实现可以满足这些操作。curry

让我们来测试一下:

const join = (a: number, b: number, c: number) => {
  return `${a}_${b}_${c}`;
};

const curriedJoin = curry(join);
// const curriedJoin: Curried<[a: number, b: number, c: number], string>

const first = curriedJoin(1);
// const first: Curried<[b: number, c: number], string>

const s = first(2, 3);
// const s: string
console.log(s); // "1_2_3"

curriedJoin(1)(2, 3); // '1_2_3'

看起来不错,一切都如愿以偿。


但有一点需要注意。任何依赖于函数的长度属性的代码都不能在 TypeScript 的类型系统中完美表示。TypeScript 的立场是,可以使用比参数数量更多的参数安全地调用函数,至少在回调的可分配性方面是这样。请参阅文档常见问题解答。因此,您可以拥有一个与 TypeScript 知道的参数数量不一致的函数。这意味着您最终可能会遇到这种情况:length

function myFilter(predicate: (value: string, index: number, array: string[]) => unknown) {
  const strs = ["a", "bc", "def", "ghij"].filter(predicate);
  console.log(strs);
  const curried = curry(predicate);
  curried("a")(2);
}

myFilter()接受字符串数组的 filter() 方法接受的类型的回调。此回调将使用三个参数调用;这就是 JavaScript 的工作方式。但 TypeScript 允许你传递使用较少参数的回调。所以你可以这样称呼:filter()

myFilter(x => x.length < 3) // ["a", "bc"], followed by RUNTIME ERROR! 

任何地方都没有编译器错误,但您会收到运行时错误。认为回调的正文长度为 ,但实际上在运行时的长度为 。哎呀。myFilter31

这可能不会发生在您的实际代码中,但您应该意识到这一点。

Playground 代码链接