【问题标题】:Class with property, where the property itself contains a property of type <T extends Class>具有属性的类,其中属性本身包含 <T extends Class> 类型的属性
【发布时间】:2019-08-20 17:15:49
【问题描述】:

我正在编写一个Engine 类,该类在构造时获取Module 类的映射,并通过自身实例化它们。然后将创建的实例存储在modules 中。我使用泛型来确保引擎上存在的模块是已知的,并且可以被接受Engine 类型参数的函数声明为期望的。

这个Engine 类还包含地图事件处理程序,它需要一个将Engine 实例作为其第一个参数的回调。处理程序可以通过公共方法注册。

模块可能如下所示:

class Renderer extends Module<{Renderer: Renderer}> {
    constructor(engine: Engine<{Renderer: Renderer}>) {
        super(engine);

        this.engine.addEventHandler('draw', drawSomething);
    }

    renderText(message: string) {
        console.log(message);
    }
}

...其中drawSomething,事件处理程序,看起来像这样:

function drawSomething(engine: Engine<{Renderer: Renderer}>): void {
    engine.modules.Renderer.renderText('hello world');
}

你可以发现整个问题都解决了here in TypeScript playground,错误出现在第 54 行。我还用only the type definitions 创建了一个问题,以查看我想要的概念的形状。

我遇到的问题是,当我注册事件处理程序drawSomething时,在检查处理程序是否正确键入时,我传递的泛型似乎被遗忘了。假设处理程序将在没有Renderer 作为已知模块的情况下被调用。打字稿告诉我:

类型 'Engine' 不可分配给类型 'Engine'

我如何确保 TypeScript 知道当我将事件处理程序注册到具有某些模块的Engine 实例时,处理程序也将传递相同的@ 987654335@ 实例在被调用时带有那些模块?

我正在使用 TypeScript 3.5.3。

【问题讨论】:

    标签: typescript typescript-generics


    【解决方案1】:

    关键问题是Module 的递归类型定义,它接受另一个必须是自身子类型的类型。

    原码

    class Module<TModulesOfEngine extends Modules = {}> {
        engine: Engine<TModulesOfEngine>;
    
        constructor(engine: Engine<TModulesOfEngine>) {
            this.engine = engine;
        }
    }
    
    class Engine<TModules extends Modules = {}> {
        public modules: TModules;
        public eventHandlers: {
            [eventName: string]: EventHandler<TModules>[];
        } = {};
    
        constructor(modules: ConstructableModules<TModules> = {} as ConstructableModules<TModules>) {
            this.modules = Object.keys(modules).reduce((allModules: TModules, moduleName: string): TModules => {
                const ModuleClass = modules[moduleName];
                return {
                    ...allModules,
                    [moduleName]: new ModuleClass(this),
                }
            }, {} as TModules);
        }
    ...
    

    在某些情况下,可以使用polymorphic this type,如下例所示:Typescript - Generic type extending itself

    但是this类型不能用在静态方法或者构造函数中,也就是说下面是错误的。

    class Module {
        engine: Engine<this>;
    
        // Error: A 'this' type is available only in a non-static member of a class or interface.
        constructor(engine: Engine<this>) { 
            this.engine = engine;
        }
    }
    

    虽然不美观,但以下递归类型定义有效。

    解决方案

    class Module<T extends Module<T>> {
        engine: Engine<T>;
    
        constructor(engine: Engine<T>) {
            this.engine = engine;
        }
    }
    
    class Engine<T extends Module<T>> {
        public modules: ModuleMap<T>;
        public eventHandlers: {
           [eventName: string]: EventHandler<T>[];
        } = {};
    
        constructor(modules: ConstructableModule<ModuleMap<T>>) {
            this.modules = Object.keys(modules).reduce((allModules: ModuleMap<T>, moduleName: string): ModuleMap<T> => {
                const ModuleClass = modules[moduleName];
                return {
                    ...allModules,
                    [moduleName]: new ModuleClass(this),
                }
            }, {} as ModuleMap<T>);
        }
    
        addEventHandler(name: string, handler: EventHandler<T>): void {
            this.eventHandlers = {
                ...this.eventHandlers,
                [name]: [
                    ...(this.eventHandlers[name] || []),
                    handler,
                ],
            };
        }
    }
    

    请参阅Typescript Playground 上的工作示例。

    【讨论】:

    • 啊,当然!所以,一般来说,这意味着:type EventHandler&lt;TModulesOfEngine extends Modules = {}&gt; = (engine: Engine&lt;TModulesOfEngine&gt;, ...args: any[]) =&gt; void; ...但现在我在尝试传递泛型时遇到了一些其他问题:“Type '{ Renderer: Renderer; }' 不满足约束 'Modules' . 类型 'Renderer' 不可分配给类型 'Module'。"
    • 这可能是一个解决方案,但是我每次使用它时都需要强制Engine 期待模块,而不是在构造已知模块后使用它。您最初关于 EventHandler 上缺少泛型的建议对于完全解决它非常有帮助,因为它表明我并没有在任何地方传递泛型。
    • @bakkerjoeri 查看更新的答案,支持泛型和一个有效的 Typescript Playground 示例。
    • 太棒了!感谢您的详尽解释和替代方案。
    【解决方案2】:

    Modules 接口的原始定义打破了泛型链。任何使用它都会使 TypeScript 失去之前定义的 Engine.modules 类型。由于它不接受任何泛型传递给Module,因此无法捕获Engine 上的预期模块:

    class Module<TModulesOfEngine extends Modules = {}> {
        engine: Engine<TModulesOfEngine>;
    
        constructor(engine: Engine<TModulesOfEngine>) {
            this.engine = engine;
        }
    }
    
    // This is missing generic arguments, making it impossible to preserve typing.
    interface Modules {
        [moduleName: string]: Module;
    }
    

    要解决此问题,Modules 也需要开始接受 TModulesOfEngine

    class Module<TModulesOfEngine extends Modules<TModulesOfEngine> = {}> {
        engine: Engine<TModulesOfEngine>;
    
        constructor(engine: Engine<TModulesOfEngine>) {
            this.engine = engine;
        }
    }
    
    interface Modules<TModulesOfEngine extends Modules<TModulesOfEngine> = {}> {
        [moduleName: string]: Module<TModulesOfEngine>;
    }
    

    完整的实现可以在this TypeScript playground找到。

    我通过删除类型默认值(例如TModules extends Modules = {})发现了忘记传递泛型的位置。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-07-04
      • 1970-01-01
      • 1970-01-01
      • 2020-08-08
      • 1970-01-01
      • 1970-01-01
      • 2021-07-29
      • 1970-01-01
      相关资源
      最近更新 更多