如何在编译时修补类型?

时间:2018-06-01 08:32:02

标签: typescript

有没有办法实现在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);

但这种风格感觉倒退,它仍然无法解决无法在不返回新类型的情况下重新定义类型的问题。

有没有办法通过方法调用在编译时更改类型?

1 个答案:

答案 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