TypeScript:接受不同泛型的数组并返回合并类型

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

TypeScript: accept an array of different generics and return a merged type

问题

我试图弄清楚实体组件系统,为了提高可用性,我需要能够传递不同组件定义的数组并获得合并类型作为结果。

我试图做到这一点:

const Position = defineComponent({ x: 0, y: 0 });
const Container = defineComponent({ capacity: 0, contents: new Array<{ item: number; amount: number }>() });

world.withComponents(Position, Container).forEach(([id, entity]) => {
  console.log(entity.x);
  console.log(entity.capacity);
});

完整代码:

interface ISchema {}

type SimpleType = number | string | boolean;

type Type<V> = V extends SimpleType
  ? V
  : V extends Array<infer RT>
  ? Array<Type<RT>>
  : V extends ISchema
  ? ComponentType<V>
  : never;

type ComponentType<T extends ISchema> = {
  [key in keyof T]: Type<T[key]>;
};

type ComponentDefinition<T extends ISchema> = {
  id: Symbol;
  type: ComponentType<T>;
};

function defineComponent<T extends ISchema>(schema: T): ComponentDefinition<T> {
  return {
    id: Symbol(),
    type: schema as ComponentType<T>,
  };
}

type TupleToIntersection<T extends any[]> = {
  [I in keyof T]: (x: T[I]) => void;
}[number] extends (x: infer I) => void
  ? I
  : never;

class World {
  protected readonly entities = new Map<number, { data: ISchema; components: Set<Symbol> }>();

  withComponents<T extends ComponentType<ISchema>>(
    definitions: ComponentDefinition<T>[]
  ): Array<[number, TupleToIntersection<T[]>]> {
    const result: Array<[number, TupleToIntersection<T[]>]> = [];
    for (const [id, entity] of this.entities) {
      let isMatch = true;

      for (const def of definitions) {
        if (!entity.components.has(def.id)) {
          isMatch = false;
          break;
        }
      }

      if (isMatch) {
        result.push([id, entity.data as TupleToIntersection<T[]>]);
      }
    }

    return result;
  }
}

目前,我在这一行上遇到以下错误:

world.withComponents([Position, Container])
类型 'ComponentDefinition<{ capacity: number; contents: { item: number; amount: number; }[]; }>' 的参数不能赋给类型 'ComponentDefinition<{ x: number; y: number; }>' 的参数。
类型 'ComponentType<{ capacity: number; contents: { item: number; amount: number; }[]; }>' 中缺少属性 'x'、'y' 的类型。

所以我的问题是我无法弄清楚如何接受不同泛型的数组。

英文:

I'm trying to figure out Entity Component System and for usability I need to be able to pass an array of different Component definitions and get a merged type as a result.

I'm trying to do this:

const Position = defineComponent({ x: 0, y: 0 });
const Container = defineComponent({ capacity: 0, contents: new Array&lt;{ item: number; amount: number }&gt;() });

world.withComponents(Position, Container).forEach(([id, entity]) =&gt; {
  console.log(entity.x);
  consone.log(entity.capacity);
});

Full code:

interface ISchema {}

type SimpleType = number | string | boolean;

type Type&lt;V&gt; = V extends SimpleType
  ? V
  : V extends Array&lt;infer RT&gt;
  ? Array&lt;Type&lt;RT&gt;&gt;
  : V extends ISchema
  ? ComponentType&lt;V&gt;
  : never;

type ComponentType&lt;T extends ISchema&gt; = {
  [key in keyof T]: Type&lt;T[key]&gt;;
};

type ComponentDefinition&lt;T extends ISchema&gt; = {
  id: Symbol;
  type: ComponentType&lt;T&gt;;
};

function defineComponent&lt;T extends ISchema&gt;(schema: T): ComponentDefinition&lt;T&gt; {
  return {
    id: Symbol(),
    type: schema as ComponentType&lt;T&gt;,
  };
}

type TupleToIntersection&lt;T extends any[]&gt; = {
  [I in keyof T]: (x: T[I]) =&gt; void;
}[number] extends (x: infer I) =&gt; void
  ? I
  : never;

class World
{
  protected readonly entities = new Map&lt;number, { data: ISchema; components: Set&lt;Symbol&gt; }&gt;();

  withComponents&lt;T extends ComponentType&lt;ISchema&gt;&gt;(
    definitions: ComponentDefinition&lt;T&gt;[]
  ): Array&lt;[number, TupleToIntersection&lt;T[]&gt;]&gt; {
    const result: Array&lt;[number, TupleToIntersection&lt;T[]&gt;]&gt; = [];
    for (const [id, entity] of this.entities) {
      let isMatch = true;

      for (const def of definitions) {
        if (!entity.components.has(def.id)) {
          isMatch = false;
          break;
        }
      }

      if (isMatch) {
        result.push([id, entity.data as TupleToIntersection&lt;T[]&gt;]);
      }
    }

    return result;
  }
}

Currently I getting the following error on this line:

world.withComponents([Position, Container])
Argument of type &#39;ComponentDefinition&lt;{ capacity: number; contents: { item: number; amount: number; }[]; }&gt;&#39; is not assignable to parameter of type &#39;ComponentDefinition&lt;{ x: number; y: number; }&gt;&#39;.
Types of property &#39;type&#39; are incompatible.
Type &#39;ComponentType&lt;{ capacity: number; contents: { item: number; amount: number; }[]; }&gt;&#39; is missing the following properties from type &#39;ComponentType&lt;{ x: number; y: number; }&gt;&#39;: x, yts(2345)

So my problem is that I'm not able to figure out how to accept an array of different generics.

答案1

得分: 1

以下是代码部分的翻译:

You can make withComponents() generic in the tuple type T of arguments to ComponentDefinition&lt;&gt; for each element of the definitions input. That is, if definitions is of type [ComponentDefinition&lt;X&gt;, ComponentDefinition&lt;Y&gt;, ComponentDefinition&lt;Z&gt;], then T would be [X, Y, Z]. Then you can make the type of definitions be a mapped type over the tuple T, and the compiler can infer T from definitions because it's a homomorphic mapped type (see https://stackoverflow.com/q/59790508/2887218). Like this:

withComponents() 泛型 应用到 ComponentDefinition&lt;&gt; 的参数元组类型 T 中,用于处理 definitions 输入的每个元素。也就是说,如果 definitions 的类型是 [ComponentDefinition&lt;X&gt;, ComponentDefinition&lt;Y&gt;, ComponentDefinition&lt;Z&gt;],那么 T 就会是 [X, Y, Z]。然后,可以将 definitions 的类型设定为元组 T 上的映射类型,编译器可以从 definitions 推断出 T,因为它是同态映射类型(参见 https://stackoverflow.com/q/59790508/2887218)。就像这样:

withComponents&lt;T extends ComponentType&lt;ISchema&gt;[]&gt;(
        definitions: [...{ [I in keyof T]: ComponentDefinition&lt;T[I]&gt; }] 
): Array&lt;[number, TupleToIntersection&lt;T&gt;]&gt;

So { [I in keyof T]: ComponentDefinition&lt;T[I]&gt; } is the relevant mapped type. And I've wrapped it in a variadic tuple type [...+], which, as the implementing pull request microsoft/TypeScript#39094 mentions, "can conveniently be used to indicate a preference for inference of tuple types".

所以 { [I in keyof T]: ComponentDefinition&lt;T[I]&gt; } 是相关的映射类型。而且我已将它包裹在一个变参元组类型 [...+] 中,正如实现的 pull request microsoft/TypeScript#39094 中所提到的,它可以方便地用于指示对元组类型的推断偏好。

And everywhere else in your code that uses T will need to be updated to account for the fact that it's [X, Y, Z] and not X | Y | Z... that is, every TupleToIntersection&lt;T[]&gt; should be replaced with TupleToIntersection&lt;T&gt;.

而在你的代码的其他地方使用 T 的地方都需要更新,以考虑它是 [X, Y, Z] 而不是 X | Y | Z...也就是说,每个 TupleToIntersection&lt;T[]&gt; 都应该替换为 TupleToIntersection&lt;T&gt;


Let's try it out:

让我们试一试:

const Position = defineComponent({ x: 0, y: 0 });
const Container = defineComponent({ capacity: 0, contents: new Array&lt;{ item: number; amount: number }&gt;() });
const world = new World();
world.withComponents([Position, Container]).forEach(([id, entity]) =&gt; {
    console.log(entity.x);
    console.log(entity.capacity);
});

Looks good!

看起来不错!

[Playground link to code](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgJIGUEAsIFs7IDeyAvgFBlgCeADiusLjQDYQAqtKAvMiAK64ARtGQAfZAGcwUUAHMxyQQHslrOCADcFanWQc6AHgBqAPmQ8jyCAA9IIACYTkDJq30QyyL8gD8yI57eAFz+VrYQDk4AglBQcFQGoDAiAEpsJoFefjFxCe4GaSYZ3sghljZ2jmiYOPiZvsgAwkpMSiARYPmm9SHtAG7QWpScTS00bR35bGGVThjYeHBmPIT1ANoA1hBUyKDIW1RKMHoAuiFTm9snJlokQzooza3t4AAiEDCgwGDAbQbTFQiVXmtSW5iI9WA9hC6CoQlUWhKDxCT3GL06nH+NzIdwoMD4IAQPzayHsH1AEFRE3A-xmQLmNUWJgAFBIFvhzgBKFFjalgd6fEDfX4gLEQkpQCBgPhQEDikreKEwuHKZjMzkAGnqSM4ITZoOQcCcVPRUxMWpKuPIw10bD4LHYSlQ4GgEggRJFtMBkUNICoa2u4NWJTWqF2coOR1OIWZ1nOoZOnPMZj6SihtzW-CE0BOdJ9sZCSREqCTXBTafs9T8qB6vAgAygQwQzCNTgA6kooMx7PKvDQoEpIESID3JXB7G1mDsOsKIE4eO0AO7IACycBoBizwigGqIpLgYDgIRBiw0

英文:

You can make withComponents() generic in the tuple type T of arguments to ComponentDefinition&lt;&gt; for each element of the definitions input. That is, if definitions is of type [ComponentDefinition&lt;X&gt;, ComponentDefinition&lt;Y&gt;, ComponentDefinition&lt;Z&gt;], then T would be [X, Y, Z]. Then you can make the type of definitions be a mapped type over the tuple T, and the compiler can infer T from definitions because it's a homomorphic mapped type (see https://stackoverflow.com/q/59790508/2887218). Like this:

withComponents&lt;T extends ComponentType&lt;ISchema&gt;[]&gt;(
definitions: [...{ [I in keyof T]: ComponentDefinition&lt;T[I]&gt; }] 
): Array&lt;[number, TupleToIntersection&lt;T&gt;]&gt;

So { [I in keyof T]: ComponentDefinition&lt;T[I]&gt; } is the relevant mapped type. And I've wrapped it in a variadic tuple type [...+], which, as the implementing pull request microsoft/TypeScript#39094 mentions, "can conveniently be used to indicate a preference for inference of tuple types".

And everywhere else in your code that uses T will need to be updated to account for the fact that it's [X, Y, Z] and not X | Y | Z... that is, every TupleToIntersection&lt;T[]&gt; should be replaced with TupleToIntersection&lt;T&gt;.


Let's try it out:

const Position = defineComponent({ x: 0, y: 0 });
const Container = defineComponent({ capacity: 0, contents: new Array&lt;{ item: number; amount: number }&gt;() });
const world = new World();
world.withComponents([Position, Container]).forEach(([id, entity]) =&gt; {
console.log(entity.x);
console.log(entity.capacity);
});

Looks good!

Playground link to code

huangapple
  • 本文由 发表于 2023年3月7日 02:30:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/75654544.html
匿名

发表评论

匿名网友

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

确定