有问题输入`zip`,以及通常在使用泛型的重载函数中预测推断失败。

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

Trouble typing `zip`, and generally predicting when inference breaks in overloaded functions with generics

问题

I sometimes have this kind of issue but I don't know exactly why the inference does not work.

In this case zip returns the correct type when applied directly, but not when applied through pipe and I have no idea why.

declare const zip: {
    <A, B>(input: readonly [readonly A[], readonly B[]]): [A, B][],
    <A, B, C>(input: readonly [readonly A[], readonly B[], readonly C[]]): [A, B, C][],
    <A, B, C, D>(input: readonly [readonly A[], readonly B[], readonly C[], readonly D[]]): [A, B, C, D][],
    <A extends readonly any[]>(input: readonly A[]): A[0][][],
};

declare const pipe:  <A, B>(a: A, f: (a: A) => B) => B;

declare const x: [number[], string[], boolean[]];

const foo = pipe(x, zip);
//     ^? foo: any[][]
const bar = pipe(x, x => zip(x))
//     ^? bar: [number, string, boolean][]

It's not that big of a deal, but it really puzzles me as it can lead to time wasted understanding why something does not compile, or efforts wasted trying to type something for it to not behave as intended in the end.

This pipe function, by the way, is an extract of the following, which works in most cases (not this one obviously), but is a little hard to look at.

const pipe: {
    <A>(
        a: A
    ): A
    <A, B>(
        a: A,
        f: (a: A) => B
    ): B
    <A, B, C>(
        a: A,
        f: (a: A) => B,
        g: (b: B) => C
    ): C
    // ... and so on for more parameters
}

You may think that, hey, with a mapped type you can come up with something like this (don't pay attention to the details).

import { Lift, free, Init, Tail } from 'free-types';

declare const pipe: {
    <A,B>(a: A, ...fns: Fns<[A, B]>): B
    <A,B, C>(a: A, ...fns: Fns<[A, B, C]>): C
    // ... and so on for more parameters
};

type Fns<Fs extends [unknown, ...unknown[]]> = Lift<free.UnaryFunction, [Init<Fs>, Tail<Fs>]>

However, this mapped type approach is only going to work on monomorphic functions for some reason. I experimented with simpler designs, but basically at one point there will be one indirection that will lose the compiler, and I can't really predict it. The longer/simpler version of pipe is the one you find in most functional programming libraries, and for good reason apparently.

Can you explain the zip situation? If you have thoughts on the more involved pipe version, feel free to share them.

英文:

I sometimes have this kind of issue but I don't know exactly why the inference does not work.

In this case zip returns the correct type when applied directly, but not when applied through pipe and I have no idea why.

declare const zip: {
    &lt;A, B&gt;(input: readonly [readonly A[], readonly B[]]): [A, B][],
    &lt;A, B, C&gt;(input: readonly [readonly A[], readonly B[], readonly C[]]): [A, B, C][],
    &lt;A, B, C, D&gt;(input: readonly [readonly A[], readonly B[], readonly C[], readonly D[]]): [A, B, C, D][],
    &lt;A extends readonly any[]&gt;(input: readonly A[]): A[0][][],
};

declare const pipe:  &lt;A, B&gt;(a: A, f: (a: A) =&gt; B) =&gt; B;

declare const x: [number[], string[], boolean[]];

const foo = pipe(x, zip);
//     ^? foo: any[][]
const bar = pipe(x, x =&gt; zip(x))
//     ^? bar: [number, string, boolean][]

playground
It's not that big of a deal, but it really puzzles me as it can leat to time wasted understanding why something does not compile, or efforts wasted trying to type something for it to not behave as intended in the end.

This pipe function by the way is an extract of the following, which works in most cases (not this one obviously), but is a little hard to look at

const pipe: {
    &lt;A&gt;(
        a: A
    ): A
    &lt;A, B&gt;(
        a: A,
        f: (a: A) =&gt; B
    ): B
    &lt;A, B, C&gt;(
        a: A,
        f: (a: A) =&gt; B,
        g: (b: B) =&gt; C
    ): C
    &lt;A, B, C, D&gt;(
        a: A,
        f: (a: A) =&gt; B,
        g: (b: B) =&gt; C,
        h: (c: C) =&gt; D
    ): D
    &lt;A, B, C, D, E&gt;(
        a: A,
        f: (a: A) =&gt; B,
        g: (b: B) =&gt; C,
        h: (c: C) =&gt; D,
        i: (d: D) =&gt; E
    ): E
    &lt;A, B, C, D, E, F&gt;(
        a: A,
        f: (a: A) =&gt; B,
        g: (b: B) =&gt; C,
        h: (c: C) =&gt; D,
        i: (d: D) =&gt; E,
        j: (e: E) =&gt; F
    ): F
    &lt;A, B, C, D, E, F, G&gt;(
        a: A,
        f: (a: A) =&gt; B,
        g: (b: B) =&gt; C,
        h: (c: C) =&gt; D,
        i: (d: D) =&gt; E,
        j: (e: E) =&gt; F,
        k: (f: F) =&gt; G
    ): G
    &lt;A, B, C, D, E, F, G, H&gt;(
        a: A,
        f: (a: A) =&gt; B,
        g: (b: B) =&gt; C,
        h: (c: C) =&gt; D,
        i: (d: D) =&gt; E,
        j: (e: E) =&gt; F,
        k: (f: F) =&gt; G,
        l: (g: G) =&gt; H
    ): H
    &lt;A, B, C, D, E, F, G, H, I&gt;(
        a: A,
        f: (a: A) =&gt; B,
        g: (b: B) =&gt; C,
        h: (c: C) =&gt; D,
        i: (d: D) =&gt; E,
        j: (e: E) =&gt; F,
        k: (f: F) =&gt; G,
        l: (g: G) =&gt; H,
        m: (h: H) =&gt; I
    ): I
    &lt;A, B, C, D, E, F, G, H, I, J&gt;(
        a: A,
        f: (a: A) =&gt; B,
        g: (b: B) =&gt; C,
        h: (c: C) =&gt; D,
        i: (d: D) =&gt; E,
        j: (e: E) =&gt; F,
        k: (f: F) =&gt; G,
        l: (g: G) =&gt; H,
        m: (h: H) =&gt; I,
        n: (i: I) =&gt; J
    ): J
}

You may think that, hey, with a mapped type you can come up with something like this (don't pay attention to the details)

import { Lift, free, Init, Tail } from &#39;free-types&#39;;

declare const pipe: {
    &lt;A,B&gt;(a: A, ...fns: Fns&lt;[A, B]&gt;): B
    &lt;A,B, C&gt;(a: A, ...fns: Fns&lt;[A, B, C]&gt;): C
    &lt;A,B, C, D&gt;(a: A, ...fns: Fns&lt;[A, B, C, D]&gt;): D
    &lt;A,B, C, D, E&gt;(a: A, ...fns: Fns&lt;[A, B, C, D, E]&gt;): E
    &lt;A,B, C, D, E, F&gt;(a: A, ...fns: Fns&lt;[A, B, C, D, E, F]&gt;): F
    &lt;A,B, C, D, E, F, G&gt;(a: A, ...fns: Fns&lt;[A, B, C, D, E, F, G]&gt;): G
    &lt;A,B, C, D, E, F, G, H&gt;(a: A, ...fns: Fns&lt;[A, B, C, D, E, F, G, H]&gt;): H
    &lt;A,B, C, D, E, F, G, H, I&gt;(a: A, ...fns: Fns&lt;[A, B, C, D, E, F, G, H, I]&gt;): I
    &lt;A,B, C, D, E, F, G, H, I, J&gt;(a: A, ...fns: Fns&lt;[A, B, C, D, E, F, G, H, I, J]&gt;): J
};

type Fns&lt;Fs extends [unknown, ...unknown[]]&gt; = Lift&lt;free.UnaryFunction, [Init&lt;Fs&gt;, Tail&lt;Fs&gt;]&gt;

playground

but it is only going to work on monomorphic functions for some reason. I experimented with simpler designs, but basically at one point there will be one indirection that will loose the compiler and I can't really predict it. The longer/simpler version of pipe is the one you find in most FP libraries, and for good reason apparently.

Can you explain the zip situation? If you have thoughts on the more involved pipe version, feel free to share them.

答案1

得分: 1

TL;DR:对于重载函数的推断会忽略任何泛型并返回最后一个调用签名。这是 TypeScript 的设计限制。

由于 zip() 是一个重载函数,涉及它的推断只考虑最后一个调用签名,而不是选择你直接调用 zip() 时得到的“适当”调用签名。这是 TypeScript 的已知设计限制,如 microsoft/TypeScript#50432 中所述(还有其他一些问题)。

所以你得到的行为类似于如果 zip 只是这样的:

declare const zip: {
  <A extends any[]>(input: A[]): A[0][][],
};

虽然在这种情况下,你实际上会得到一个更具体的结果:

declare const x: [number[], string[], boolean[]];
const foo = pipe(x, zip);
//     ^? const foo: (string | number | boolean)[][]

但因为你有重载,推断实际上擦除泛型类型参数在匹配时,如 microsoft/TypeScript#52916 中所述(这可能是一个 bug,但根据那个问题,可能没有好的改变方式),因此类型参数被替换为其约束。这意味着它实际上的行为是:

declare const zip: {
  (input: any[][]): any[][0][][],
};

这会产生:

const foo = pipe(x, zip);
//     ^? const foo: any[][]

正如你所见。

英文:

TL;DR Inference to overloaded functions gives you the last call signature with any generics erased. It's a design limitation of TypeScript.


Since zip() is an overloaded function with multiple call signatures, inferences involving it only consider the last call signature, instead of choosing the "appropriate" call signature that you'd get when you call zip() directly. This is a known design limitation of TypeScript, as described in microsoft/TypeScript#50432 (among others).

So you get behavior similar to what would happen if zip were just

declare const zip: {
&lt;A extends any[]&gt;(input: A[]): A[0][][],
};

Although in that case, you'd actually get a more specific result:

declare const x: [number[], string[], boolean[]];
const foo = pipe(x, zip);
//     ^? const foo: (string | number | boolean)[][]

But because you have overloads, the inference actually erases the generic type parameter when matching, as described in microsoft/TypeScript#52916 (which might be a bug but there's probably no good way to change it, at least according to that issue), and therefore the type parameter is replaced with its constraint. Meaning it actually behaves like

declare const zip: {
(input: any[][]): any[][0][][],
};

which produces

const foo = pipe(x, zip);
//     ^? const foo: any[][]

as you've seen.


Playground link to code

huangapple
  • 本文由 发表于 2023年7月23日 19:54:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/76748085.html
匿名

发表评论

匿名网友

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

确定