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
”。这也是我在这里的建议。
您的第二个错误是由于您在 Wizard
中声明的选项字段导致的隐藏 您在 Human
中声明的选项字段。这就是大多数(所有)基于 class 的类型系统的工作方式,而不仅仅是 TS。因此 Wizard.options
在 this.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
的所有必需道具负全部责任。
扩展 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
”。这也是我在这里的建议。
您的第二个错误是由于您在 Wizard
中声明的选项字段导致的隐藏 您在 Human
中声明的选项字段。这就是大多数(所有)基于 class 的类型系统的工作方式,而不仅仅是 TS。因此 Wizard.options
在 this.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
的所有必需道具负全部责任。