【问题标题】:Can TypeScript return the subclass if a function has the superclass as its return type?如果函数将超类作为其返回类型,TypeScript 可以返回子类吗?
【发布时间】:2019-10-26 19:23:01
【问题描述】:

问题

错误: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(); 

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

【问题讨论】:

    标签: typescript inheritance


    【解决方案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 类型以表示您在 cmets 中隐含的键值映射,并使 getComponent() 成为泛型方法,其中 key 参数是泛型类型 K 来自 keyof ComponentContainer,它返回一个ComponentContainer[K] 类型的值(这是当你look up K 对象的K 属性时得到的)。像这样:

    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];
        }
    }
    

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

    // Player subclass of GameActor
    class Player extends GameActor {
        bloop() {
            this.getComponent("EventComponent").attach(PlayerDeathEvent.create(this)); // okay
    
        }
    }
    

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

    Link to code

    【讨论】:

    • 这非常有效,并且在概念上对我的架构有意义。干杯。
    猜你喜欢
    • 2018-12-28
    • 1970-01-01
    • 1970-01-01
    • 2021-05-01
    • 2013-10-17
    • 2019-01-15
    • 1970-01-01
    • 1970-01-01
    • 2017-04-06
    相关资源
    最近更新 更多