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
TID
when trying to resolve the type oftraitData.id
expression. Consequently, the only information it has at the moment is the constraint to... extends TraitId
. That's why TS resolvestraitData.id
expression toTraitId
orTraitId.Movable | TraitId.Scalable | TraitId.Rotatable | TraitId.Collidable
instead ofTID
.The simplest way to achieve the goal is to use the modified variant of
addTraitToEntity2
function with existing types:Negative aspects:
as any
is 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
id
member with an incorrectdata
member and vice versa.Meantime, I refactored your code:
TS Playground
Improvements:
Entity
was introduced as an abstract class which includes API for add, remove, retrieve traits;Entity
class;Entity
class is now generic and you can customize which traits entity can have;