如果函数将父类作为其返回类型,则TypeScript可以返回子类吗?

时间:2019-10-26 19:23:01

标签: typescript inheritance

问题

错误:Property 'attach' does not exist on type 'Component'. 如何返回存储在自定义字典类型中的任意子类,并使用仅在该字典类型具有超类的返回值的情况下才存在于子类中的方法?

上下文

我有一个Component类,其中包含许多子类。 我的GameActor类可以附加组件。附加组件存储在ComponentContainer成员中,该成员是一种自定义类型,可以是Component的任何子类。例如。 MovementComponentHealthComponentEventComponent等都可以存储在ComponentContainer中。

当我检索附加的组件时,尝试从检索到的组件中调用方法时,出现上面的“属性不存在”错误。如果打开浏览器的开发工具,我会看到返回类型的日志看起来像实际上是子类类型

ComponentContainer类型定义

//globals.d.ts
// ComponentContainer type definition
// populated data structure will look like:
// {
//   "EventComponent": EventComponent,
//   "MovementComponent": MovementComponent,
//   "FooComponent": FooComponent
//   // etc...
// }
type ComponentContainer = {
  [componentName: string]: Component;
}

GameActor有很多方法可以添加,删除,获取和列出所连接的组件。


//GameActor class that can have Components attached to it
export abstract class GameActor extends GameObject implements IGameActor {

  protected components: ComponentContainer;

  constructor() {
    this.components = {};
  }

  public getComponent(key: string): Component|null {
      return this.components[key];
  }
}

// Player subclass of GameActor
export class Player extends GameActor implements IPlayer {
  //Player class code here...
}

//Component superclass
export abstract class Component {
  protected typeId: string;

  public getTypeId(): string {
    return this.typeId;
  }
}

//EventComponent subclass
export class EventComponent extends Component implements IEventComponent {

  public attach(event: Event|CustomEvent): void {
    //Attach code here...
  }
}

现在在代码中的其他地方,我想执行以下操作:

this.getComponent("EventComponent").attach(PlayerDeathEvent.create(this));

这时我收到错误。如果我注销以下代码,则它们看起来都是EventComponent类型。

let ec = this.Player.getComponent("EventComponent");
let t = new EventComponent(); 

browser console log

我希望.attach不会抛出错误,因为编译器知道该组件的类型为EventComponent

1 个答案:

答案 0 :(得分:1)

这里的问题是编译器没有将this.getComponent("EventComponent")的结果从Component | null缩小到EventComponent所需的信息。方法getComponent()被声明为采用string并返回Component | null,因此编译器才真正知道这一切。

的确,编译器会进行一定数量的control flow analysis运算,其中会根据使用它们的方式将值的类型细化为更具体的类型,以声明它们的方式……但这只会在非常具体的情况和is by no means perfect。编译器无法查看任意代码,也无法在运行之前弄清楚运行时将发生什么。好吧,technically nobody can do that。但是,是的,在很多情况下,编译器都不了解人类显而易见的东西。这是一个简化的示例:

function hmm(x: number) {
    return (x >= 0) ? x : "negative";
}

console.log(hmm(4).toFixed()); // error!
// --------------> ~~~~~~~
// "toFixed" does not exist on number | "negative"

编译器仅知道hmm()接受number并返回number | "negative"。对于人类来说,很明显hmm(4)将返回一个数字,因此将具有一个toFixed()方法,并且在运行时可以肯定的是,代码可以正确运行。但是编译器不知道hmm(4)最终会评估4 >= 0,也不知道4 >= 0最终会返回true,因此不会。不知道hmm(4)不会返回"negative",这意味着它不知道hmm(4)将具有toFixed()方法,这意味着...编译器错误。


如果您希望编译器通过更强的类型明确地提供信息,则不会期望编译器从代码流中找出结果,而是将获得更好的结果。我的建议是增强您的ComponentContainer类型以表示您在注释中隐含的键-值映射关系,并使getComponent()为通用方法,其中key参数为通用类型{{1 }}从K中返回,并且返回类型为keyof ComponentContainer的值(当您look up ComponentContainer[K]对象的K属性时得到的值)。像这样:

ComponentContainer

现在,当您调用方法时,它应该可以按您期望的方式工作:

type ComponentContainer = {
    EventComponent: EventComponent;
    // MovementComponent: MovementComponent,
    // FooComponent: FooComponent
    // etc
}

abstract class GameActor {

    protected components: ComponentContainer;

    constructor() {
        // better actually initialize this properly
        this.components = {
            EventComponent: new EventComponent()
            // MovementComponent: new MovementComponent();
            // FooComponent: new FooComponent();
            // etc
        }
    }

    public getComponent<K extends keyof ComponentContainer>(key: K): ComponentContainer[K] {
        return this.components[key];
    }
}

好的,希望能有所帮助;祝你好运!

Link to code