You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript实现ECS:接收不同泛型组件数组并返回合并类型的问题

解决ECS中withComponents方法接收多类型Component的类型问题

你的核心问题是withComponents方法当前的泛型定义只能接受单一类型的ComponentDefinition数组,无法兼容多个不同类型的组件定义。下面是具体的修复方案:

关键问题分析

原方法的泛型T extends ComponentType<ISchema>强制要求数组中所有ComponentDefinition的type必须完全一致,但Position和Container是两种不同的组件类型,自然会触发类型不匹配错误。

修复步骤

1. 调整泛型定义为可变元组

将withComponents的泛型改为接收可变元组类型,这样可以容纳多个不同的ComponentDefinition,同时改用rest参数支持直接传入多个组件(无需包裹数组):

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

  withComponents<T extends ComponentDefinition<ISchema>[]>(
    ...definitions: T
  ): Array<[number, TupleToIntersection<{ [I in keyof T]: T[I]['type'] }>]> {
    const result: Array<[number, TupleToIntersection<{ [I in keyof T]: T[I]['type'] }>]> = [];
    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<{ [I in keyof T]: T[I]['type'] }>]);
      }
    }

    return result;
  }
}

2. 优化类型提取逻辑

通过{ [I in keyof T]: T[I]['type'] }从元组T中提取每个组件的type,再利用已有的TupleToIntersection工具类型将这些类型交叉合并,最终得到包含所有组件属性的实体类型。

3. 验证调用效果

现在你可以直接调用world.withComponents(Position, Container),类型系统会正确推断出entity包含x、y、capacity和contents属性:

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 ComponentDefinition<ISchema>[]>(
    ...definitions: T
  ): Array<[number, TupleToIntersection<{ [I in keyof T]: T[I]['type'] }>]> {
    const result: Array<[number, TupleToIntersection<{ [I in keyof T]: T[I]['type'] }>]> = [];
    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<{ [I in keyof T]: T[I]['type'] }>]);
      }
    }

    return result;
  }
}

// 测试代码
const Position = defineComponent({ x: 0, y: 0 });
const Container = defineComponent({ capacity: 0, contents: new Array<{ item: number; amount: number }>() });
const world = new World();

// 调用无类型错误,实体属性类型正确
world.withComponents(Position, Container).forEach(([id, entity]) => {
  console.log(entity.x);
  console.log(entity.capacity);
});

内容的提问来源于stack exchange,提问作者user64675

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 05:53:18