有没有办法实现在TypeScript中创建此样式的组件系统所需的动态类型?
let block = new Entity();
// block.components === {};
block.has(new Position(0, 0));
// block.components === { position: { ... } }
Entity#components
没有索引签名,而是密钥解析为适当的组件类型的严格形状。
以下是对实施的粗略看法:
class Position implements Component {
name = "position";
x = 0;
y = 0;
}
interface Component {
name: string;
[key: string]: any;
}
class Entity<T={}> {
components: T;
has(component: Component) {
type ExistingComponents = typeof this.components;
type NewComponents = { [component.name]: Component };
this.components[component.name] = component;
return this as Entity<ExistingComponents & NewComponents>;
}
}
有许多原因导致这实际上无效:
has
方法返回具有修改类型的原始实体,而不是更改现有类型。NewComponents
类型无法编译,因为它依赖于运行时属性(component.name
)。我考虑的另一个解决方案是将扩展作为组件的一部分来实现,因此名称可以是静态的:
class Position implements Component {
name = "position";
x = 0;
y = 0;
static addTo(entity: Entity) {
type Components = typeof entity.components & { position: Position };
entity.components.position = new Position();
return entity as Entity<Components>;
}
}
let position = new Position(0, 0);
position.addTo(block);
但这种风格感觉倒退,它仍然无法解决无法在不返回新类型的情况下重新定义类型的问题。
有没有办法通过方法调用在编译时更改类型?
答案 0 :(得分:1)
我们需要将Component
更改为通用,以便在编译类型中保留表示组件名称的字符串文字类型。然后使用一些条件类型,我们可以实现所需的结果:
// T will be the name of the component (ex 'position')
interface Component<T> {
name: T;
[key: string]: any;
}
// Conditional type to extract the name of the component (ex: ComponentName<Position> = 'position')
type ComponentName<T extends Component<any>> = T extends Component<infer U> ? U : never;
class Entity<T={}> {
components: T;
// TNew is the added component type
// We return Entity with the original T and add in a new property of ComponentName<TNew> which will be of type TNew with a mapped type
has<TNew extends Component<any>>(componentInstance: TNew) : Entity<T & { [ P in ComponentName<TNew>] : TNew }> {
this.components[componentInstance.name] = componentInstance;
return this as any;
}
}
//Usage:
export class Position implements Component<'position'> {
readonly name = "position";
x = 0;
y = 0;
}
export class Rectangle implements Component<'rectengle'> {
readonly name = "rectengle";
x = 0;
y = 0;
}
let block = new Entity().has(new Position()).has(new Rectangle());
block.components.rectengle //ok
block.components.position // ok