从静态属性推断 Typescript 泛型

时间:2021-02-18 18:18:39

标签: typescript typescript-generics

我正在编写一个库,用户可以在其中定义抽象类 State 的子类。 State 的任何子类都将“依赖”(在库的逻辑内)从抽象类 Component 继承的许多类。这些依赖项必须在代码中静态声明,以便我的库可以预先分析它们。我还想根据 State 继承的用户类定义的依赖项对它们的方法进行类型检查。下面是一个例子:

class ComponentA extends Component {}
class ComponentB extends Component {}

class ExampleState extends State {
  static dependencies = [ComponentA, ComponentB] as const;

  // this method should correctly type the tuple provided to this
  // user-defined method as containing an instance of ComponentA
  // in position 0 and an instance of ComponentB in position 1, ordered as
  // provided in the static dependencies property.
  initialize(components) {
    const [a, b] = components;
    console.assert(a instanceof ComponentA); // true
  }
}

具体来说,我想避免强迫用户也将泛型传递给 State,例如 State<[typeof ComponentA, typeof ComponentB]>,这既冗长又多余。但是,我不能依赖泛型,因为我需要运行时的依赖信息。

是否有任何选项可以在 TypeScript 中定义它?还是静态与实例太分离而无法共享这种类型的信息?是否有另一种模式(可能除了类之外)可以为这种用法实现正确的开发人员人体工程学?

更新

经过大量实验,我发现了至少一种可能的用法,它可以保持人体工程学(根据指定的依赖项在 initialize 方法上键入提示),而无需代码冗余:

TS Playground Link

不过,用法还是有点别扭。用户必须提供一个函数来创建他们的伪类对象,并使用一个闭包来存储他们通常会简单地附加到他们的子类的“实例属性”。

我仍然很想知道是否有人可以提出更简化的解决方案,但如果没有,我至少会自己回答这个问题,这是可行的。

更新 2:关于我的目标的更多细节

我正在开发一个用于创建网页游戏的框架库,其灵感来自/扩展了用于游戏开发的 ECS 模式。我想添加到 ECS 的部分是框架用户或补充库作者的一种方式,可以根据附加到任何特定实体的组件的某些组合来定义声明性状态管理。

作为使用该框架编写游戏的用户,我想启用一种模式,例如“如果任何实体在游戏生命周期的任何时间点都附加了组件 [Image, Position, Color],则 Sprite”状态”对象应该由从这些组件派生数据的框架构建。”

为此,用户将向实现 initialize/destroy 接口的框架提供状态定义。这些状态定义将负责提供命令式逻辑,以在游戏逻辑代码中启用声明式使用。

这是我理想情况下想要启用的使用草图,使用假设的 SomeSpriteThing 资源,类似于来自 PixiJS 或 ThreeJS 的资源,例如:

class SpriteState extends State {
  static dependencies = [Image, Position, Color] as const;

  private sprite = new SomeSpriteThing();

  initialize([image, position, color]) {
    this.sprite.src = image.src;
    this.sprite.x = position.x;
    this.sprite.y = position.y;
    this.sprite.tint = color.value;
    this.sprite.load();
  }

  destroy() {
    this.sprite.unload();
  }
}

库用户会向框架提供这样的状态类,每当一个实体被分配到 [Image, Position, Color] 组件时,框架都会生成一个 SpriteState 并提供给游戏逻辑代码。< /p>

但是,在上述理想用法中,initialize 无法从 dependencies 静态推断提供的元组的类型。

作为一个以 Typescript 为重点的库,我希望用户编写的代码能够被彻底检查类型,而无需他们在指定冗余类型方面付出过多的努力。这就是为什么我想避免强迫用户向 State 超类中的泛型参数提供相同的依赖类型列表 - 不仅不方便,而且没有办法强制 {{1 }} 和泛型保持同步,因为一个只对代码可见,另一个对类型检查可见。

我意识到这不是我以前见过的模式,并且可能是过度设计的。也许这就是它的全部。但是我很好奇是否有一些可能的方法可以通过类型安全来解决这个问题。

1 个答案:

答案 0 :(得分:1)

<块引用>

是否有任何选项可以在 TypeScript 中定义它?还是静态与实例过于分离而无法共享此类信息?

当您创建类 ExampleState 时,您还创建了一个类型 ExampleState,它描述了该类的实例。静态属性适用于 ExampleState,因为它们不是实例属性。每个类还有第二种类型,typeof ExampleStatenew () => ExampleState,它描述了类的构造函数。这是定义静态属性的地方。 (relevant docs section)

<块引用>

是否有另一种模式(可能除了类之外)可以为这种用法实现正确的开发人员人体工程学?

我的建议是使用某种高阶函数来创建您的类。我很难做到完全正确,因为我不完全了解用例,但这至少应该为您指明正确的方向。

这个接口描述了我们想要的构造函数。它可以被实例化以创建一个 State<Deps>,它还有一个 dependencies 类型的属性 Deps

interface StateCreator<Deps extends ComponentType<any>[]> {
    new (): State<Deps>;
    dependencies: Deps;
}

此函数接受您的依赖项并返回一个匿名类,该类将这些依赖项作为静态属性。

function makeState<Deps extends ComponentType<any>[]>(dependencies: Deps): StateCreator<Deps> {
    return class extends State<Deps> {
        static dependencies: Deps = dependencies;

        initialize(components: Deps) {
            //do something
        }
    }
}

我有点不明白您对 initialize 的意图。如果从静态属性中已知 Deps,我们为什么要提供 Deps 作为参数?您的意思是要取 ComponentAComponentB实例吗?