创建和返回一个通用对象数组的通用函数

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

Generic function for creating and returning a generic array of objects

问题

在这段Typescript代码中,你遇到了一个类型问题,TypeScript 不喜欢你尝试将对象推送到 createAssets 函数内的 assets 数组中。它显示以下错误消息:

Argument of type '{ id: U; image: {}; texts: {}; }' is not assignable to parameter of type 'T'.
  'T' could be instantiated with an arbitrary type which could be unrelated to '{ id: U; image: {}; texts: {}; }'.

这个问题是由于 TypeScript 无法正确推断 T 的类型,因为它可能与 { id: U; image: {}; texts: {}; } 不相关。为了解决这个问题,你可以使用 TypeScript 的泛型约束来明确告诉 TypeScript T 应该是什么类型。

你可以将 createAssets 函数修改如下:

const createAssets = <T extends { id: U; image: any; text: any }, U>(ids: U[]): T[] => {
    const assets: T[] = [];

    for (const dataId of ids) {
        assets.push({
            id: dataId,
            image: {},
            text: {},
        });
    }

    return assets;
}

在这里,我们使用 <T extends { id: U; image: any; text: any }, U> 来指定 T 必须是具有特定属性的对象类型。这样,TypeScript 就能够正确推断 T 的类型,并且你的代码将不再产生错误。

请注意,我已经将 text 更正为 texts,以匹配你的类型定义。你可以根据你的需求进行修改。希望这对你有所帮助。如果还有其他问题,请随时提出。

英文:

Everytime I think I have understood the generics, I come up with a task which proves me wrong. This is what I want:

Playground

type tFruits  = &#39;apple&#39; | &#39;orange&#39; | &#39;pear&#39;;
type tCars    = &#39;peugot&#39; | &#39;mercedes&#39;| &#39;nissan&#39;;
type tFlowers = &#39;rose&#39; | &#39;tulip&#39; | &#39;violet&#39;;

type tAssets = {
  image: {},
  text: {},
}

type tFruitAssets  = tAssets &amp; { id: tFruits }
type tCarAssets    = tAssets &amp; { id: tCars }
type tFlowerAssets = tAssets &amp; { id: tFlowers }


const createAssets = &lt;T, U&gt;(ids: U[]):T[] =&gt; {
    const assets:T[] = [];

    for (const dataId of ids){
      // Typescript doesn&#39;t like this:
        assets.push({
            id: dataId,
            image:{},
            text:{},
        });
    }

    return assets;
}

const fruitAssets  = createAssets&lt;tFruitAssets, tFruits&gt;([&#39;apple&#39;,&#39;orange&#39;,&#39;pear&#39;]);
const carAssets    = createAssets&lt;tCarAssets, tCars&gt;([&#39;peugot&#39;,&#39;mercedes&#39;,&#39;nissan&#39;]);
const flowerAssets = createAssets&lt;tFlowerAssets, tFlowers&gt;([&#39;rose&#39;,&#39;tulip&#39;,&#39;violet&#39;]);

Typescript doesn't like the object I try to push to the assets array inside my createAssets functions. It says:

Argument of type &#39;{ id: U; image: {}; texts: {}; }&#39; is not assignable to parameter of type &#39;T&#39;.
&#39;T&#39; could be instantiated with an arbitrary type which could be unrelated to &#39;{ id: U; image: {}; texts: {}; }&#39;.

Could someone please explain me how I have to change the function? Or is it not possible?

答案1

得分: 1

createAssets 函数的作用是从 U[] 创建 (tAssets & { id: U })[],因此其类型应该是:

const createAssets = <U>(ids: U[]): (tAssets & { id: U })[] => {
    const assets: (tAssets & { id: U })[] = [];
    for (const dataId of ids) {
        assets.push({
            id: dataId,
            image: {},
            text: {},
        });
    }
    return assets;
};
英文:

Because T could be other type like tAssets &amp; { id: tFruits } &amp; { anotherId: tFruits } which is not compatible with tAssets &amp; { id: tFruits } like the createAssets do.

createAssets say it can create T[] from U[], but it actually only create (tAssets &amp; { id: U })[] from U[].If T includes anotherId, createAssets will not return it and this may cause error.

type tFruits = &#39;apple&#39; | &#39;orange&#39; | &#39;pear&#39;;
type tAssets = {
    image: {};
    text: {};
};
type FruitAssets = tAssets &amp; { id: tFruits };
type OtherFruitAssets = FruitAssets &amp; { anotherId: tFruits };
//works
const tFruitAssets: FruitAssets[] = createAssets&lt;FruitAssets, tFruits&gt;([&#39;apple&#39;]);
//not work properly,but will not throw type error here
//expect otherTFruitAssets to be type OtherFruitAssets[]
//but actually otherTFruitAssets will be FruitAssets[] and this may cause error
const otherTFruitAssets: OtherFruitAssets[] = createAssets&lt;OtherFruitAssets, tFruits&gt;([&#39;apple&#39;]);

What createAssets do is create (tAssets &amp; { id: U })[] from U[], so its type should be:

const createAssets = &lt;U,&gt;(ids: U[]): (tAssets &amp; { id: U })[] =&gt; {
    const assets: (tAssets &amp; { id: U })[] = [];
    for (const dataId of ids) {
        assets.push({
            id: dataId,
            image: {},
            text: {},
        });
    }
    return assets;
};

答案2

得分: 0

10分钟后,我想出了这个:

// [...]

const createAssets = <T, U>(ids: U[]): T[] => {
    const assets: T[] = [];

    for (const dataId of ids) {
        const t = {
            id: dataId,
            image: {},
            text: {},
        } as T;
        assets.push(t);
    }

    return assets;
}

Typescript 现在满足要求了,但我觉得这更像是一种权宜之计,而不是一个正式的解决方案。我是不是在自欺欺人?

英文:

10 Minutes later I came up with this:

// [...]

const createAssets = &lt;T, U&gt;(ids: U[]):T[] =&gt; {
    const assets:T[] = [];

    for (const dataId of ids){
        const t = {
            id: dataId,
            image:{},
            text:{},
        } as T;
        assets.push(t);
    }

    return assets;
}

Typescript is satisfied now but I feel like as if it would be more of a workaround than a proper solution. Am I fooling myself here?

答案3

得分: 0

以下是翻译好的内容:

"While you could assert it and be done with it for now, this is actually unsound because the caller chooses the type of T and U, which means this passes type-checking:

const fruitAssets = createAssets&lt;tCarAssets, tFruits&gt;([&quot;apple&quot;, &quot;orange&quot;, &quot;pear&quot;]);
//                               ^^^^^^^^^^ should be invalid

You can easily make it safer by adding constraints to the generic parameters.

const createAssets = &lt;T extends tAssets &amp; { id: U }, U extends string&gt;(ids: U[]): T[] =&gt; {

However, now you will have to use as unknown as T because TypeScript sees the possibility that someone may use

//                                               ^^^^^^^^^^^^^ extra property here

which means that pushing t to assets is unsound, since you would be missing that extra property. If you really wanted to, you could also prevent this from happening by changing the constraints a little...

const createAssets = &lt;T extends tAssets &amp; { id: U }, U extends (tAssets &amp; { id: U } extends T ? string : never)&gt;(ids: U[]): T[] =&gt; {

Unfortunately, with all of these in place, the assertion is still necessary. You could try to make it a little cleaner (get rid of the double assertion) with map:

const createAssets = &lt;T extends tAssets &amp; { id: U }, U extends (tAssets &amp; { id: U } extends T ? string : never)&gt;(ids: U[]): T[] =&gt; {
    const assets = ids.map((id) =&gt; ({ id, image: {}, text: {} })) as T[];

    return assets;
};
英文:

While you could assert it and be done with it for now,

const createAssets = &lt;T, U&gt;(ids: U[]): T[] =&gt; {
    const assets: T[] = [];

    for (const dataId of ids) {
        const t = {
            id: dataId,
            image: {},
            texts: {},
        } as T;

        assets.push(t);
    }

    return assets;
};

this is actually unsound because the caller chooses the type of T and U, which means this passes type-checking:

const fruitAssets = createAssets&lt;tCarAssets, tFruits&gt;([&quot;apple&quot;, &quot;orange&quot;, &quot;pear&quot;]);
//                               ^^^^^^^^^^ should be invalid

You can easily make it safer by adding constraints to the generic parameters.

const createAssets = &lt;T extends tAssets &amp; { id: U }, U extends string&gt;(ids: U[]): T[] =&gt; {

However, now you will have to use as unknown as T because TypeScript sees the possibility that someone may use

createAssets&lt;{ id: string; image: {}; text: {}; extraProperty: {} }, string&gt;(...);
//                                               ^^^^^^^^^^^^^ extra property here

which means that pushing t to assets is unsound, since you would be missing that extra property. If you really wanted to, you could also prevent this from happening by changing the constraints a little...

const createAssets = &lt;T extends tAssets &amp; { id: U }, U extends (tAssets &amp; { id: U } extends T ? string : never)&gt;(ids: U[]): T[] =&gt; {

Unfortunately, with all of these in place, the assertion is still necessary.
You could try to make it a little cleaner (get rid of the double assertion) with map:

const createAssets = &lt;T extends tAssets &amp; { id: U }, U extends (tAssets &amp; { id: U } extends T ? string : never)&gt;(ids: U[]): T[] =&gt; {
    const assets = ids.map((id) =&gt; ({ id, image: {}, text: {} })) as T[];

    return assets;
};

Playground

huangapple
  • 本文由 发表于 2023年3月9日 23:06:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/75686438.html
匿名

发表评论

匿名网友

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

确定