【问题标题】:Inferring a Typescript generic from a static property从静态属性推断 Typescript 泛型
【发布时间】:2021-02-21 04:11:15
【问题描述】:

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

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 "State" 对象应该由从这些组件中派生数据的框架构建。”

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

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

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();
  }
}

库用户会向框架提供此类 State 类,并且每当将一个实体分配给 [Image, Position, Color] 组件时,框架将生成一个 SpriteState 并提供给游戏逻辑代码。

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

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

我认识到这不是我以前见过的模式,并且可能是过度设计的。也许这就是它的全部。但我很好奇是否有某种可能的方式来处理类型安全。

【问题讨论】:

    标签: typescript typescript-generics


    【解决方案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 作为参数?您的意思是要采取 instancesComponentAComponentB 吗?

    【讨论】:

    • 我很抱歉,在我的编辑中将我的代码示例简化为基本要素,我确实不小心混淆了组件实例和构造函数。 initialize 接收组件的实例,其构造函数作为依赖项提供。我会更新这个问题。我希望作为库的用途是能够实际定义生成的类(继承自 State),指定它静态依赖的组件构造函数,并在它们传递给initialize 时对其实例进行类型检查,用户定义。
    • 澄清一下,我认为工厂方法是一种很好的方法(我在更新问题时也有类似的想法)。但我的目标之一是让 TS 自动建议并强制为 initialize 方法按照用户定义的方式输入。该库提供了工具来帮助用户根据 dependencies 定义类型安全的 State 子类,但用户最终定义了该类。在英语中,意图是这样的:“我想定义一种依赖于这些组件的状态,并为这些组件的一组实例实现这个initialize。”
    • 我想我不太“明白”这个。我的一般倾向是组合而不是继承。我不确定我们从 State 继承了什么功能,以及我们是否可以使用组合模式将该功能与我们想要定义的 initialize 结合起来。
    • 我可能要么未能提供足够的上下文,要么我的目标过于设计。不过,如果您有兴趣,我已经添加了有关我正在尝试构建的问题的细节的更多细节。我认为 TS 可能无法实现我想要的使用模式......无论如何,我感谢您参与这个问题。如果我添加的内容很明确并且答案是“您不能那样做”,那么欢迎您将其添加到您的答案中,我会接受它,因为您的其他建议已经有助于重新定义问题.
    • 所以我玩了一段时间,因为我真的很喜欢思考设计。这段代码仍然存在问题,但它是否朝着正确的方向发展? tsplay.dev/GmZl9w
    猜你喜欢
    • 2021-12-14
    • 1970-01-01
    • 2021-09-29
    • 2020-07-23
    • 1970-01-01
    • 2020-03-11
    • 2021-11-14
    • 2017-10-04
    • 2019-09-17
    相关资源
    最近更新 更多