我正在编写一个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 <{Renderer:Renderer; }>'
如何确保TypeScript知道,当我将事件处理程序注册到具有某些模块的Engine
实例时,该处理程序也将被传递调用这些模块的同一Engine
实例吗?
我正在使用TypeScript 3.5.3。
答案 0 :(得分: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上的工作示例。
答案 1 :(得分:0)
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 = {}
)发现了默认值,该默认值揭示了在哪里遗忘了泛型的传递。