Class 和 属性,其中 属性 本身包含类型 <T extends Class> 的 属性

Class with property, where the property itself contains a property of type <T extends Class>

我正在写一个 Engine class,它在构造时采用 Module classes 的映射并实例化它们通过自身。然后将创建的实例存储在 modules 中。我使用泛型来确保引擎上存在的模块是已知的,并且可以被采用 Engine.

类型参数的函数声明为预期的

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

模块可能如下所示:

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, with the error at line 54. I've also created one with only the type definitions,看看我想要的概念的形状。

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

Type 'Engine<{}>' is not assignable to type 'Engine<{ Renderer: Renderer; }>'

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

我正在使用 TypeScript 3.5.3。

关键问题是 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 could be used, as shown in this example:

但是,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 上的工作示例。

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 = {})找到了答案,它揭示了忘记传递泛型的地方。