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
相关产品推荐
相关产品推荐

