如何修复递归函数的“类型的所有组成部分都不可调用”错误?

huangapple go评论92阅读模式
英文:

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

问题

这是您提供的代码的翻译部分:

所以我有一个用于柯里化其他函数的函数:
```javascript
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); // 应该返回 '1_2_3',但会报错

TypeScript 不允许我多次调用它,因为第一次调用可能返回一个字符串。您要如何修复它?

英文:

So I have a function for currying other functions:

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

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

const curriedJoin = curry(join);

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

Typescript doesn't let me call it more than once cause the first call might've returned a string. How would you fix it?

答案1

得分: 0

以下是翻译好的部分:

唯一能够使这个方法生效的方式是 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;
}

因此,Curried<A, R> 表示一个函数的柯里化版本,其参数列表为 A,返回类型为 R。您可以使用一些泛型剩余参数类型 AA 来调用它,这些参数必须可以赋值给 A 的部分版本(使用Partial 实用程序类型)。然后,我们需要找出参数列表的剩余部分,它被infer 推断为 AR(请注意,也许 A extends [...AA, ...infer AR] ? ⋯ 可能会起作用,但如果由于某种原因 AAA 的初始部分更窄,那就会失败。所以 {[I in keyof AA]: any} 只是意味着“与 AA 长度相同的任何内容”。如果 AR 为空([]),则柯里化函数返回 R。否则,它返回 Curried<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'

看起来不错,所有都按预期工作。

但是有一个注意事项。任何依赖于函数的 length 属性的代码无法在 TypeScript 的类型系统中完全表示。TypeScript 认为函数可以安全地接受比参数数量更多的参数,至少在涉及回调的可赋值性方面是如此。请参阅文档常见问题解答。因此,您可以有一个函数,其 length 与 TypeScript 所知的参数数量不一致。这意味着您可能会陷入以下情况:

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() 方法 接受的类型相同的回调。这个回调将被调用三个参数,这就是 filter() 在 JavaScript 中的工作方式。但 TypeScript 允许您传递接受较少参数的回调。因此,您可以调用这个:

myFilter(x => x.length < 3) // ["a", "bc"],接着是运行时错误!

没有编译器错误,但会得到运行时错误。myFilter 的主体认为回调的长度是 3,但实际上运行时长度为 1。糟糕。

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

英文:

The only way this could work is if curry() returns a function whose output type depends significantly on the number of parameters in the input function and the number of arguments passed in. That means you will need it to be not only generic, but also use conditional types to express the difference. And you'll need the type of that function to be recursive, so we have to give it a name:

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

So a Curried&lt;A, R&gt; represents a curried version of a function whose parameter list is A and whose return type is R. You call it with some args of a generic rest parameter type AA, which must be assignable to a partial version of A (using the Partial&lt;T&gt; utility type). Then we need to figure out the rest of the parameter list, which is inferred to be AR (Note that perhaps A extends [...AA, ...infer AR] ? ⋯ would work, but if for some reason AA is narrower than the initial part of A, that would fail. So {[I in keyof AA]: any} just means "anything that's the same length as AA"). If AR is empty ([]) then the curried function returns R. Otherwise it returns Curried&lt;AR, R&gt;, so that the resulting function can be called also.

The implementation could look like:

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

Note that I used the any type and a type assertion to convince the compiler that the implementation of curry is acceptable. The compiler can't really understand many higher-order generic type operations, or verify what function implementations might satisfy them.

Let's test it out:

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

const curriedJoin = curry(join);
// const curriedJoin: Curried&lt;[a: number, b: number, c: number], string&gt;

const first = curriedJoin(1);
// const first: Curried&lt;[b: number, c: number], string&gt;

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

curriedJoin(1)(2, 3); // &#39;1_2_3&#39;

Looks good, that all behaves as desired.


But there's a caveat. Any code that depends on the length property of a function cannot be perfectly represented in TypeScript's type system. TypeScript takes the position that functions can safely be called with more arguments than the number of parameters, at least when it comes to assignability of callbacks. See the documentation and the FAQ. So you can have a function whose length does not agree with the number of parameters TypeScript knows about. Meaning you can possibly end up in this situation:

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

myFilter() takes a callback of the type accepted by an array of strings' filter() method. This callback will be called with three arguments; that's how filter() works in JavaScript. But TypeScript lets you pass callbacks that take fewer arguments. So you can call this:

myFilter(x =&gt; x.length &lt; 3) // [&quot;a&quot;, &quot;bc&quot;], followed by RUNTIME ERROR! 

There's no compiler error anywhere, but you get a runtime error. The body of myFilter thinks the callback has a length of 3, but it really has a length of 1 at runtime. Oops.

This might not happen in your actual code, but you should be aware of it.

Playground link to code

huangapple
  • 本文由 发表于 2023年7月31日 22:11:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/76804448.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定