Typescript 继承:扩展基础 class 对象 属性

Typescript Inheritance: Expanding base class object property

扩展 class 时,我可以轻松地向其添加一些新属性。

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

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

基础class

type HumanOptions = {
  alive: boolean
  age: number
}

class Human {
  id: string
  options: HumanOptions

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

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

  }
}

派生class

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.
  }
}

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

也许您想知道为什么我想要 options 对象中的那些属性。它可能非常有用!例如:只有options中的属性被某些UI编辑。因此,一些其他代码可以在 class 中动态查找 options 并在 UI 中动态查找 expose/update,同时 id 和 [=19= 等属性] 会一个人呆着。

备注

我知道我可以为向导做这件事:

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"
    }
  }
}

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

如果您只是要重用来自 superclass 的 属性 但将其视为更窄的类型,您可能应该 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 fields are at Stage 4 of the TC39 process and will be part of ES2022 (currently in draft at 2021-12-07). When that happens (or maybe now if you have a new enough JS engine),你写的代码最终会产生 JavaScript 看起来像

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

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

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

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


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

所以你可能会争辩说,如果你没有使用 --useDefineForClassFields,编译器不应该给你一个错误。在--useDefineForClassFields存在之前,这样的争论实际上是在microsoft/TypeScript#20911 and microsoft/TypeScript#21175.

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

Playground link to code

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

来查看

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

请参阅 以了解我认为对此案例的最佳解决方案。

您可以通过以下方式进行: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 的所有必需道具负全部责任。