ECS架构中如何实现仅靠泛型参数获取对应Component实例?
问题描述
我想要编写一个简单的Entity Component System(ECS),现有代码如下:
class Component {} class Entity { readonly components: Array<Component> = [] } class T extends Component {} class F extends Component { get hello() { return 'world' } } class World { readonly entities: Array<Entity> = [] readonly components: Map<typeof Component, Array<Component>> = new Map() first_entity(T: typeof Entity) { return this.entities.find(_ => _ instanceof T) } all_entities(T: typeof Entity) { return this.entities.filter(_ => _ instanceof T) } first<T extends Component>(ctor: { new(...args: any[]): T }) { return this.components.get(ctor) } constructor() { this.components.set(T, [new T()]) this.components.set(F, [new F(), new F()]) console.log(this.first<F>(F)) } } let world = new World()
我希望能这样调用:
world.first<F>() // 返回第一个F类型的Component实例
目前的实现需要同时传入泛型参数和构造函数(如world.first<F>(F)),但我希望仅通过泛型参数完成调用。此外,需要保证类型安全:world.first<F>()?.hello可以正确编译,而world.first<T>()?.hello无法编译。
解决方案
TypeScript泛型在运行时会被擦除,无法直接从泛型类型获取构造函数引用,我们可以通过维护类型名称与构造函数的映射来实现无参数调用,同时保证类型安全,以下是两种可行方案:
方案一:手动维护类型映射
适合组件类型固定的场景,直接在first方法中关联泛型类型与构造函数:
class Component {} class Entity { readonly components: Array<Component> = [] } class T extends Component {} class F extends Component { get hello() { return 'world' } } class World { readonly entities: Array<Entity> = [] readonly components: Map<typeof Component, Array<Component>> = new Map() first_entity(T: typeof Entity) { return this.entities.find(_ => _ instanceof T) } all_entities(T: typeof Entity) { return this.entities.filter(_ => _ instanceof T) } first<C extends Component>(): C | undefined { // 手动关联组件类型名称与构造函数 const ctorMap = { T: T, F: F } as const; const ctor = ctorMap[C.name as keyof typeof ctorMap]; if (!ctor) return undefined; // 类型断言保证返回值类型正确 const comps = this.components.get(ctor) as Array<C>; return comps?.[0]; } constructor() { this.components.set(T, [new T()]) this.components.set(F, [new F(), new F()]) console.log(this.first<F>()) // 直接通过泛型调用 } } let world = new World() // 类型安全验证 world.first<F>()?.hello; // 编译通过 world.first<T>()?.hello; // 编译报错(T无hello属性),符合预期
方案二:自动注册维护映射
适合组件类型动态新增的场景,通过注册方法自动记录类型与构造函数的关联:
class Component {} class Entity { readonly components: Array<Component> = [] } class T extends Component {} class F extends Component { get hello() { return 'world' } } class World { readonly entities: Array<Entity> = [] private readonly componentNameMap = new Map<string, typeof Component>() readonly components: Map<typeof Component, Array<Component>> = new Map() first_entity(T: typeof Entity) { return this.entities.find(_ => _ instanceof T) } all_entities(T: typeof Entity) { return this.entities.filter(_ => _ instanceof T) } // 注册组件时自动记录类型名称与构造函数的映射 registerComponent<C extends Component>(ctor: { new(): C }, instances: Array<C>) { this.componentNameMap.set(ctor.name, ctor); this.components.set(ctor, instances); } first<C extends Component>(): C | undefined { const ctor = this.componentNameMap.get(C.name) as { new(): C }; if (!ctor) return undefined; const comps = this.components.get(ctor) as Array<C>; return comps?.[0]; } constructor() { this.registerComponent(T, [new T()]); this.registerComponent(F, [new F(), new F()]); console.log(this.first<F>()); } } let world = new World() // 类型安全验证 world.first<F>()?.hello; // 编译通过 world.first<T>()?.hello; // 编译报错,符合预期
核心说明
- 利用类的
name属性作为泛型类型与构造函数的关联键,绕开泛型擦除的限制。 - 通过类型断言
as Array<C>让TypeScript正确推导返回值类型,确保类型安全:存在对应属性的组件调用能通过编译,不存在的则报错。
内容的提问来源于stack exchange,提问作者eguneys
相关产品推荐
相关产品推荐

