I'm writing some abstract entity system for fun where I have entities with traits. Traits have some fields, including dynamic data field:
enum TraitId {
Movable = 'Movable', Rotatable = 'Rotatable', Scalable = 'Scalable', Collidable = 'Collidable'
}
interface TraitDataMovable {
x: number;
y: number;
}
type TraitDataMap = {
[TraitId.Movable]: TraitDataMovable
, [TraitId.Rotatable]: number // angle
, [TraitId.Scalable]: number // scale
, [TraitId.Collidable]: boolean // collides or not
}
interface TraitData<ID extends TraitId> {
id: ID;
data: TraitDataMap[ID];
disabled?: boolean;
}
type EntityTraits = {
[TID in TraitId]: TraitData<TID>
}
class Entity {
id: string;
traits: Partial<EntityTraits> = {};
}
So far I achieved correct behavior with manual assignment:
const ent = new Entity();
ent.traits.Rotatable = {
id: TraitId.Rotatable, // id can only be Rotatable
data: 100 // data can only be number
};
ent.traits.Collidable = {
id: TraitId.Collidable, // id can only be Collidable
data: true // data can only be boolean
}
const hasCollision = ent.traits.Collidable.data; // correctly typed as boolean
And now I'm trying to write function that adds any trait to the entity:
function addTraitToEntity(entity: Entity, traitData: TraitData<TraitId>) {
entity.traits[traitData.id] = traitData;
// Type 'TraitData<TraitId>' is not assignable to type 'undefined'.
}
function addTraitToEntity2<TID extends TraitId>(entity: Entity, traitData: TraitData<TID>) {
entity.traits[traitData.id] = traitData;
// Type 'TraitData<TID>' is not assignable to type 'Partial<EntityTraits>[TID]'.
// Type 'TraitData<TID>' is not assignable to type 'undefined'
}
They work with // @ts-ignore, however I'd like to get rid of it and do it right. And understand how to have such system typed correctly.
The problem is the following. TS somehow loses information about
TIDwhen trying to resolve the type oftraitData.idexpression. Consequently, the only information it has at the moment is the constraint to... extends TraitId. That's why TS resolvestraitData.idexpression toTraitIdorTraitId.Movable | TraitId.Scalable | TraitId.Rotatable | TraitId.Collidableinstead ofTID.The simplest way to achieve the goal is to use the modified variant of
addTraitToEntity2function with existing types:Negative aspects:
as anyis not a good way to solve issues;Or as @Linda Paiste suggested, but as a generic function. Because without type variable you would be able to pass specific
idmember with an incorrectdatamember and vice versa.Meantime, I refactored your code:
TS Playground
Improvements:
Entitywas introduced as an abstract class which includes API for add, remove, retrieve traits;Entityclass;Entityclass is now generic and you can customize which traits entity can have;