【问题标题】:Typescript Inheritance: Expanding base class object property打字稿继承:扩展基类对象属性
【发布时间】:2022-01-11 22:24:52
【问题描述】:

当扩展一个类时,我可以很容易地向它添加一些新属性。

但是,当我扩展一个基类时,如果我想为基类的一个对象(一个简单对象的属性)添加新属性怎么办?

这是一个带有一些代码的示例。

基类

type HumanOptions = {
  alive: boolean
  age: number
}

class Human {
  id: string
  options: HumanOptions

  constructor() {
    this.id = '_' + Math.random()

    this.options = {
      alive: true,
      age: 25,
    }

  }
}

派生类

type WizardOptions = {
  hat: string
} & HumanOptions

class Wizard extends Human{
 
  manaLevel: number
  options: WizardOptions // ! Property 'options' has no initializer and is not definitely assigned in the constructor.

  constructor() {
    super()
    this.manaLevel = 100
    this.options.hat = "pointy" // ! Property 'options' is used before being assigned.
  }
}

现在,正如您从嵌入式 cmets 中看到的那样,这会激怒 TypeScript。 但这适用于 JavaScript。那么实现这一目标的正确 ts 方法是什么?如果没有,是我的代码模式本身的问题吗?什么模式适合这个问题?

也许你想知道为什么我想要options 对象中的这些属性。它可能非常有用!例如:只有options 中的属性可以通过某些UI 进行编辑。因此,其他一些代码可以在类中动态查找 options 并在 UI 上公开/更新它们,同时诸如 idmanaLevel 之类的属性将被单独保留。

注意

我知道我可以为向导做到这一点:

class Wizard extends Human{
 
  manaLevel: number

  options: {
    hat: string
    alive: boolean
    age: number
  }

  constructor() {
    super()
    this.manaLevel = 100
    this.options = {
      alive: true,
      age: 25,
      hat: "pointy"
    }
  }
}

而且它有效。但是代码不是很干,我必须手动管理选项继承(这在复杂的应用程序中并不理想)。

【问题讨论】:

  • Clorofilla,当您说“但这在 JavaScript 中有效”时,您的意思是它在运行时在由 TypeScript 编译器输出的 JavaScript 中有效吗?或者你的意思是你手工编写了类似的 JavaScript 并且它在那里工作?对于in a comment chain 的含义存在分歧。
  • 我指的是第一种情况。 TS 报错,但 JS 输出运行没有错误。

标签: typescript class object inheritance extends


【解决方案1】:

您的第二个错误是您在 Wizard 中声明的选项字段的结果隐藏您在 Human 中声明的选项字段。这就是大多数(所有)基于类的类型系统的工作方式,而不仅仅是 TS。因此Wizard.optionsthis.options.hat = "pointy" 执行时是未定义的。你可以通过注释掉options: WizardOptions来看到这一点

这在 Javascript 中有效,因为您依赖于原型继承,并且没有在 Wizard 类中定义新的 options 字段(如果有的话,它也会隐藏 Human.options

请参阅@jcalz's answer,了解我认为适合这种情况的最佳解决方案。

【讨论】:

  • 如果您解释了如何在不隐藏Human 版本的情况下保留Wizard-特定options 属性,我认为这将是一个比接受的答案更好的答案。虽然我意识到这意味着复制一些已被接受的答案,但我认为您的解释更加直接和简洁。
  • 谢谢!我没有跟进完整的解释,因为接受的答案超过了我,坦率地说,我会想出一个不同的解决方案,因为我不知道declare!也许我应该将我的解释移到接受的答案中?
  • 当您说“Wizard.optionsthis.options.hat = "pointy" 执行时未定义”时,仅当启用--useDefineForClassFields 并且使用[[Define]] 语义而不是[[Set ]] 语义。最终它也可能是真的,因为使用旧 ES 目标或故意禁用 --useDefineForClassFields 的人数可能会减少。但目前这在很大程度上取决于您的编译器选项。
  • @jcalz 但考虑到她的错误消息,对于 Clorofilla 的案例来说确实如此。另外,--useDefineForClassFields 现在不是 Typescript 的默认值了吗?
  • 不,不是,他们说“但这在 JavaScript 中有效”大概是因为编译后的 JS 没有使用--useDefineForClassFields--useDefineForClassFields 的默认值是 disabled,除非我认为你的目标是 ESNext(也许是 ES2022?不确定)。所以大部分写TS的人目前都会看到编译后的JS工作但TS仍然有错误的行为。如果您从“所以您可以争论...”开始阅读我的回答部分,那么您会看到人们抱怨此问题的 github 问题。
【解决方案2】:

如果您只是要重用超类中的属性但将其视为更窄的类型,您可能应该use the declare property modifier in the subclass 而不是重新声明该字段:

class Wizard extends Human {
  manaLevel: number
  declare options: WizardOptions; // <-- declare
  constructor() {
    super()
    this.manaLevel = 100
    this.options.hat = "pointy" // <-- no problem now
  }
}

通过在此处使用declare,您可以抑制该行的任何 JavaScript 输出。它所做的只是告诉 TypeScript 编译器,在 Wizard 中,options 属性将被视为 WizardOptions 而不仅仅是 HumanOptions


Public class fieldsat Stage 4TC39 process 并将成为 ES2022 (currently in draft at 2021-12-07) 的一部分。当这种情况发生时(或者可能是now if you have a new enough JS engine),您编写的代码最终会生成类似于

class Wizard extends Human {
    manaLevel;
    options; // <-- this will be here in JavaScript
    constructor() {
        super();
        this.manaLevel = 100;
        this.options.hat = "pointy";
    }
}

这只是没有类型注释的 TypeScript。 但是 JavaScript 中的 options 声明会将子类中的 options 初始化为 undefined 因此,在分配之前使用 this.options 确实是正确的:

console.log(new Wizard()); // runtime error!
// this.options is undefined

所以你从 TypeScript 得到的错误是一个很好的错误。


最初 TypeScript 实现了类字段,因此如果你没有显式地初始化它,仅仅声明一个字段不会有效果。这就是 TypeScript 团队认为类字段最终将在 JavaScript 中实现的方式。但他们错了。如果您的编译器选项未启用--useDefineForClassFields(请注意,如果您使--target 足够新,它将自动包含在内;请参阅microsoft/TypeScript#45653),那么它将输出JS 代码,在您的示例中不会发生运行时错误。

所以你可以争辩说,如果你不使用--useDefineForClassFields,编译器不应该给你一个错误。在--useDefineForClassFields 存在之前,microsoft/TypeScript#20911microsoft/TypeScript#21175 居然有这样的说法。

但我认为 TS 团队不再接受这一点。随着时间的推移,JavaScript 中声明的类字段被初始化为undefined 将成为常态,而不是例外,并且支持旧的不合格版本的类字段并不是任何人的优先事项。请参阅microsoft/TypeScript#35831,其中建议基本上是“使用declare”。这也是我在这里的建议。

Playground link to code

【讨论】:

  • 只是注意到类字段位于stage 4,因此已完成,尽管新规范尚未发布。
  • 好的,有机会我会编辑的。
  • 我应该把我的答案移到你的序言中吗?见@TheRubberDuck 的评论:stackoverflow.com/questions/70250095/…
  • 这是一种不同的方法,所以它可能应该作为自己的答案。
  • 好的,我刚刚引用了你的答案。
【解决方案3】:

您可以通过以下方式进行操作:playground

type HumanOptions = {
  alive: boolean
  age: number
}

class Human<Options extends HumanOptions> {
  id: string
  options: Options

  constructor() {
    this.id = '_' + Math.random()

    this.options = {
      alive: true,
      age: 25,
    } as Options
  }
}

type WizardOptions = {
  hat: string
} & HumanOptions

class Wizard extends Human<WizardOptions> {
  manaLevel: number

  constructor() {
    super()
    this.manaLevel = 100
    this.options.hat = "pointy"
  }
}

请注意,您全权负责设置options 的所有必需属性。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-08-11
    • 1970-01-01
    • 1970-01-01
    • 2018-02-10
    • 1970-01-01
    相关资源
    最近更新 更多