打字稿:对类型化地图的动态分配

时间:2020-09-27 00:59:18

标签: typescript typescript-generics

我正在写一些有趣的抽象实体系统,其中我有具有特征的实体。特质有一些字段,包括动态data字段:

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> = {};
}

到目前为止,我通过手动分配实现了正确的行为:

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

现在我正在尝试编写将任何特征添加到实体的函数:

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'
}

他们与// @ts-ignore合作,但是我想摆脱它并正确地做。并了解如何正确键入此类系统。

Link to the playground

2 个答案:

答案 0 :(得分:1)

这是一种避免错误的回旋解决方案。如果您替换了整个对象entity.traits而不是仅分配一个属性,则它将起作用,

function addTraitToEntity(entity: Entity, traitData: TraitData<TraitId>) {
    entity.traits = {
        ...entity.traits,
        [traitData.id]: traitData
    }
}

我一直在解决这个问题,但是对于您的方法为什么行不通,我并没有得到令人满意的答案,但是我认为部分问题在于打字稿理解键traitData.id匹配值traitData

答案 1 :(得分:1)

问题如下。 TS在尝试解析TID表达式的类型时会以某种方式丢失有关traitData.id的信息。因此,它目前拥有的唯一信息是对... extends TraitId的约束。这就是TS将traitData.id表达式解析为TraitIdTraitId.Movable | TraitId.Scalable | TraitId.Rotatable | TraitId.Collidable而不是TID的原因。


实现目标的最简单方法是对现有类型使用addTraitToEntity2函数的修改后的变体:

function addTraitToEntity2<TID extends TraitId>(entity: Entity, traitData: TraitData<TID>) {
  (entity.traits[traitData.id] as any) = traitData;
}

负面因素:

  • as any不是解决问题的好方法;

或按照@Linda Paiste的建议,但作为通用函数。因为没有类型变量,您将能够通过特定的id成员和错误的data成员来传递,反之亦然。

function addTraitToEntity2<TID extends TraitId>(entity: Entity, traitData: TraitData<TID>) {
    entity.traits = {
        ...entity.traits,
        [traitData.id]: traitData
    };
}

同时,我重构了您的代码:

TS Playground

改进:

  • 较少的TS类型;
  • Entity是作为抽象类引入的,其中包括用于添加,删除,检索特征的API;
  • 特质成为Entity类的成员;
  • Entity类现在是通用的,您可以自定义实体可以具有的特征;