泛型内部如果没有使用正确的类型

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

Generics inside if not taking the proper type

问题

我明白你的问题,你遇到了 TypeScript 类型错误。问题似乎出现在 mapToObject 函数中,特别是在第一个 reducer 中,TypeScript 无法正确地推断 T 的类型。为了解决这个问题,你可以使用类型断言来告诉 TypeScript T 的类型。这是修复的代码:

export function mapToObject<T extends InputMapToObject>(
    itemToConvert: T
): ReturnMapToObject<T> {
    if (itemToConvert instanceof Map) {
        // 使用类型断言告诉 TypeScript 返回值的类型是 ReturnMapToObject<T>
        return [...itemToConvert.entries()].reduce((acc, [key, value]) => {
            (acc as ReturnMapToObject<T>)[key] = value;
            return acc;
        }, {} as ReturnMapToObject<T>);
    }

    if (Array.isArray(itemToConvert)) {
        return itemToConvert.map((item) => {
            if (isRecursiveValue(item)) {
                return mapToObject(item);
            }
            return item;
        }) as ReturnMapToObject<T>[]; // 使用类型断言告诉 TypeScript 返回值的类型是 ReturnMapToObject<T> 数组
    }

    return getObjectEntries(itemToConvert).reduce((acc, [key, value]) => {
        if (isRecursiveValue(value)) {
            acc[key] = mapToObject(value);
        } else {
            acc[key] = value;
        }

        return acc;
    }, {} as ReturnMapToObject<T>);
}

// ...其他辅助函数保持不变

通过使用类型断言,你可以明确告诉 TypeScript 在特定情况下变量的类型,从而解决了类型错误问题。在这个情况下,我们使用 (acc as ReturnMapToObject<T>) 来告诉 TypeScript acc 的类型是 ReturnMapToObject<T>

希望这可以帮助你解决问题!如果你有任何其他问题,请随时提问。

英文:

I'm trying to achieve a function that serialize everything that is a Map class to an object. What I meant with that is: I'm trying to create a function that if receives an object will check every attribute and if some of them is a Map it will return an object there. Also in case that the function receives an Array of Map will return an Array of objects.

I have created this types:

type InputMapToObject =
    | Map&lt;keyof any, unknown&gt;
    | Record&lt;keyof any, unknown&gt;
    | unknown[];

type ReturnMapToObject&lt;T extends InputMapToObject&gt; = T extends Map&lt;
    infer KMap extends keyof any,
    infer VMap
&gt;
    ? Record&lt;
          KMap,
          VMap extends InputMapToObject ? ReturnMapToObject&lt;VMap&gt; : VMap
      &gt;
    : T extends object
    ? {
          [P in keyof T]: T[P] extends InputMapToObject
              ? ReturnMapToObject&lt;T[P]&gt;
              : T[P];
      }
    : T extends (infer VArray)[]
    ? VArray extends InputMapToObject
        ? ReturnMapToObject&lt;VArray&gt;[]
        : VArray[]
    : never;

This type works perfect, here a test where you can see that the type is working:

interface TestingType {
    testMap: Map&lt;string, number&gt;;
    testArray: Array&lt;number&gt;;
    testArray2: Array&lt;Map&lt;string, string&gt;&gt;;
}

const test: ReturnMapToObject&lt;TestingType&gt; = {
    testMap: {
        string: 0,
    },
    testArray: [0],
    testArray2: [{ string: &#39;string&#39; }],
};

The problem that I have is on the function mapToObject (the other functions are just helpers):

export function mapToObject&lt;T extends InputMapToObject&gt;(
    itemToConvert: T
): ReturnMapToObject&lt;T&gt; {
    if (itemToConvert instanceof Map) {
        return [...itemToConvert.entries()].reduce((acc, [key, value]) =&gt; {
            acc[key] = value;
            return acc;
        }, {} as ReturnMapToObject&lt;T&gt;);
    }

    if (Array.isArray(itemToConvert)) {
        return itemToConvert.map((item) =&gt; {
            if (isRecursiveValue(item)) {
                return mapToObject(item);
            }
            return item;
        });
    }

    return getObjectEntries(itemToConvert).reduce((acc, [key, value]) =&gt; {
        if (isRecursiveValue(value)) {
            acc[key] = mapToObject(value);
        } else {
            acc[key] = value;
        }

        return acc;
    }, {} as ReturnMapToObject&lt;T&gt;);
}

function isRecursiveValue(item: any): item is InputMapToObject {
    if (item instanceof Map) return true;
    if (Array.isArray(item)) return true;
    if (item &amp;&amp; typeof item === &#39;object&#39;) return true;
    return false;
}

function getObjectEntries&lt;T extends Record&lt;any, any&gt;&gt;(
    value: T
): [keyof any, T[keyof T]][] {
    return Object.entries(value) as [keyof any, T[keyof T]][];
}

I think the function mapToObject would work as expected with JavaScript, but I'm having type errors. For example I do have an error on the first reducer.If the input is a Map then I use the reduce method to convert the entries to a object. The problem there is that I try to type the initial object, so I do as ReturnMapToObject&lt;T&gt; but TypeScript is interpreting as that T could be anything but not, it can only be a Map.

This is the error that I'm getting doing acc[key] = value:

Element implicitly has an &#39;any&#39; type because expression of type &#39;string | number | symbol&#39; can&#39;t be used to index type &#39;object | unknown[] | Record&lt;string | number | symbol, unknown&gt;&#39;.
  No index signature with a parameter of type &#39;string&#39; was found on type &#39;object | unknown[] | Record&lt;string | number | symbol, unknown&gt;&#39;.ts(7053)
(parameter) acc: object | unknown[] | Record&lt;string | number | symbol, unknown&gt;

If TypeScript would recognize properly that T inside the Map instance if acc would be: Record&lt;string | number | symbol, unknown&gt; because as it's defined on the ReturnMapToObject if T extends Map then it will return a Record.

答案1

得分: 1

TypeScript无法通过控制流分析来缩小或重新约束通用类型参数。因此,当您检查例如itemToConvert instanceof Map时,itemToConvert的显式类型可以从T缩小为(类似于)T & Map,但类型参数T本身保持不变。

这是有充分理由的:您可以将类型视为一组所有可能可接受的值。让我们以类型string为例。如果我说值s是通用类型S extends string,并且我检查s === "abc",现在我知道 s"abc",但我实际上不了解类型 S。也许S是字符串文字类型"abc",或者它是类似"abc" | "def" | "ghi"这样的联合类型,或者它是string。所有这些都与s === "abc"一致。这就好像我从袋子(类型)中拿出一个弹珠(值),然后检查弹珠的颜色,这告诉我很多关于弹珠的信息,但对于袋子和其中包含的颜色而言,这告诉我很少。

因此,您不能像缩小值的显式类型一样“缩小”通用类型参数,至少不需要向语言添加新特性的情况下。这样的特性在microsoft/TypeScript#27808中有一个请求,您可以在其中说出类似T oneof InputMapToObject的内容,这意味着T已知准确是InputMapToObject的(假定是互斥的)联合成员之一。然后,当您检查itemToConvert instanceof Map时,您可以说T本身要么是Map<keyof any, unknown>,要么是oneof Record<keyof any, unknown> | unknown[],并且其余的代码可能会按预期编译。但目前还不是语言的一部分。

当您尝试实现依赖于通用类型参数的条件类型的通用函数时,这种限制显而易见且令人痛苦,就像您在ReturnMapToObject<T>中所做的那样。在microsoft/TypeScript#33912中,还有一个有关通用条件函数实现的开放特性请求,同样,它还不是语言的一部分。


在这里发生变化之前,您将不得不解决这个问题。假设您不想完全重构代码,最迅速的解决方法是使用诸如type assertions之类的内容,只需告诉编译器不要抱怨。如果您确信实现是正确的但编译器不是...那么这是合理的,但这也意味着您需要小心,因为如果这样做,编译器无法捕获您的错误。

以下是一种执行断言的方法:

export function mapToObject<T extends InputMapToObject>(
    itemToConvert: T
): ReturnMapToObject<T> {
    if (itemToConvert instanceof Map) {
        return [...itemToConvert.entries()].reduce((acc, [key, value]) => {
            acc[key] = value;
            return acc;
        }, {} as any); // <-- 只是使用 any
    }

    if (Array.isArray(itemToConvert)) {
        return itemToConvert.map((item) => {
            if (isRecursiveValue(item)) {
                return mapToObject(item);
            }
            return item;
        }) as ReturnMapToObject<T>; // <-- 只是断言
    }

    return getObjectEntries(itemToConvert).reduce((acc, [key, value]) => {
        if (isRecursiveValue(value)) {
            acc[key] = mapToObject(value);
        } else {
            acc[key] = value;
        }

        return acc;
    }, {} as any); // 只是使用 any
}

您可以尝试更精确的类型而不是any类型,这可能会捕获一些明显的错误。或者,您可以使函数成为单一调用签名的重载,这样也允许比调用签名更松散的实现:

function mapToObject<T extends InputMapToObject>(
    itemToConvert: T
): ReturnMapToObject<T>;

function mapToObject<T extends InputMapToObject>(itemToConvert: T):
    ReturnMapToObject<InputMapToObject> {

    if (itemToConvert instanceof Map) {
        return [...itemToConvert.entries()].
            reduce<Record<keyof any, unknown>>((acc, [key, value]) => {
                acc[key] = value;
                return acc;
            }, {});
    }

    if (Array.isArray(itemToConvert)) {
        return itemToConvert.map((item) => {
            if (isRecursiveValue(item)) {
                return mapToObject(item);
            }
            return item;
        })
    }

    return getObjectEntries(itemToConvert).
        reduce<Record<keyof any, unknown>>((acc, [key, value]) => {
            if (isRecursiveValue(value)) {
                acc[key] = mapToObject(value);
            } else {
                acc[key] = value;
            }
            return acc;
        }, {});
}

有许多此类解决方法,但如果要保留当前的实现,所有这些都需要您放宽一些类型检查,以避免这些(可能是错误的)警告。

英文:

TypeScript can't narrow or re-constrain generic type parameters via control flow analysis. So when you check, for example, itemToConvert instanceof Map, the apparent type of itemToConvert can be narrowed from T to (something like) T &amp; Map, but the type parameter T itself stays the same.

This is for a good reason: you can think of a type as something like the set of all possible acceptable values. Let's take the type string. If I say that the value s is of generic type S extends string, and I check that s === &quot;abc&quot;, now I know that the value s is &quot;abc&quot;, but I really don't know anything more about the type S. Maybe S is the string literal type &quot;abc&quot;, or maybe it's a union of such types like &quot;abc&quot; | &quot;def&quot; | &quot;ghi&quot;, or maybe it's string. All of those would be consistent with s === &quot;abc&quot;. It's like if I pull a marble (value) out of a bag (type) and then check the color of the marble, that tells me a lot about the marble, and very little about the bag and the colors of marbles it contains.

So you can't just "narrow" a generic type parameter the way you narrow the apparent type of a value, at least not without adding new features to the language. One such feature is requested at microsoft/TypeScript#27808, in which you'd be able to say something like T oneof InputMapToObject meaning that T is known to be exactly one of the (presumed mutually exclusive) union members of InputMapToObject. Then when you check whether itemToConvert instanceof Map, you can say that T itself is either Map&lt;keyof any, unknown&gt; or oneof Record&lt;keyof any, unknown&gt; | unknown[], and the rest of the code might compile as intended. But for now it's not part of the language.

This limitation is really obvious and painful when you try to implement a generic function that returns a conditional type depending on the generic type parameters, like you're doing with ReturnMapToObject&lt;T&gt;. There's another open feature request for generic conditional function implementations at microsoft/TypeScript#33912, and again, it's not yet part of the langauge.


Until and unless something changes here, you'll have to work around it. Assuming you don't want to completely refactor things, the most expedient workaround is to use something like type assertions to just tell the compiler not to complain. This is reasonable if you're sure that the implementation is correct but the compiler isn't... and it also means you need to be careful, since the compiler can't catch your mistakes if you do this.

Here's one way to do the assertions:

export function mapToObject&lt;T extends InputMapToObject&gt;(
    itemToConvert: T
): ReturnMapToObject&lt;T&gt; {
    if (itemToConvert instanceof Map) {
        return [...itemToConvert.entries()].reduce((acc, [key, value]) =&gt; {
            acc[key] = value;
            return acc;
        }, {} as any); // &lt;-- just use any
    }

    if (Array.isArray(itemToConvert)) {
        return itemToConvert.map((item) =&gt; {
            if (isRecursiveValue(item)) {
                return mapToObject(item);
            }
            return item;
        }) as ReturnMapToObject&lt;T&gt;; // &lt;-- just assert
    }

    return getObjectEntries(itemToConvert).reduce((acc, [key, value]) =&gt; {
        if (isRecursiveValue(value)) {
            acc[key] = mapToObject(value);
        } else {
            acc[key] = value;
        }

        return acc;
    }, {} as any); // just use any
}

You could try more accurate types other than the any type, which might catch some egregious errors. And/or you could make your function a single-call-signature overload which also allows a looser implementation than the call signature(s):

function mapToObject&lt;T extends InputMapToObject&gt;(
    itemToConvert: T
): ReturnMapToObject&lt;T&gt;;

function mapToObject&lt;T extends InputMapToObject&gt;(itemToConvert: T):
    ReturnMapToObject&lt;InputMapToObject&gt; {

    if (itemToConvert instanceof Map) {
        return [...itemToConvert.entries()].
            reduce&lt;Record&lt;keyof any, unknown&gt;&gt;((acc, [key, value]) =&gt; {
                acc[key] = value;
                return acc;
            }, {});
    }

    if (Array.isArray(itemToConvert)) {
        return itemToConvert.map((item) =&gt; {
            if (isRecursiveValue(item)) {
                return mapToObject(item);
            }
            return item;
        })
    }

    return getObjectEntries(itemToConvert).
        reduce&lt;Record&lt;keyof any, unknown&gt;&gt;((acc, [key, value]) =&gt; {
            if (isRecursiveValue(value)) {
                acc[key] = mapToObject(value);
            } else {
                acc[key] = value;
            }
            return acc;
        }, {});
}

There are any of a number of such workarounds, but if you want to keep your current implementation, all of them will require that you loosen some of the type checking to avoid these (presumably) false warnings.

Playground link to code

huangapple
  • 本文由 发表于 2023年6月25日 21:56:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/76550754.html
匿名

发表评论

匿名网友

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

确定