继承 JavaScript 类后,为何给已有属性赋合法值仍报类型错误?
结论
'b' 是 Letter 允许的值。报错是因为在产生这条诊断的 JavaScript 类型检查环境中,检查器将子类构造函数中赋值的属性推断为 string,而父类要求的类型是 Letter。
对于当前写法,可以在子类的赋值语句前加上 /** @type {Letter} */,为类型检查补充信息。运行时不需要重新创建一套属性。
为什么合法的值仍然报错
父类通过 JSDoc 声明了属性类型:
/** @type {Letter} */
this.property = 'a';
子类在自己的构造函数中对同名属性赋值:
this.property = 'b';
TypeScript 检查 JavaScript 时,会根据构造函数中的 this.property 赋值推断实例属性类型。当前报错反映的推断结果是:子类属性被推断为范围更宽的 string,没有沿用父类的 Letter 标注。
这两个赋值关系不同:
- 字符串字面量
'b'可以赋给Letter。 - 任意
string不能赋给Letter,因为它也可能是'd'等不允许的值。
继承检查比较的是父子类的属性类型。Y.property 被推断为 string 后,就与 X.property 的 Letter 类型不兼容。
修复代码
在子类构造函数的属性赋值前加上 JSDoc 类型标注:
/** @typedef {'a'|'b'|'c'} Letter */
class X {
constructor() {
/** @type {Letter} */
this.property = 'a';
}
}
class Y extends X {
constructor() {
super();
/** @type {Letter} */
this.property = 'b';
}
}
这样父类和子类的 property 都使用 Letter。后续如果赋值为 'd',类型检查仍然会指出错误。
是否必须在每个子类中重复标注
是否需要重复标注取决于具体的继承场景。这里需要标注,是因为 JavaScript 构造函数中属性赋值的类型推断方式。如果保留当前结构,显式标注是直接的处理方法。
如果多个子类只需要不同的初始值,也可以让父类构造函数接收一个用 @param {Letter} 标注的参数,再由子类通过 super('b') 传入。这样属性赋值集中在父类,子类就不需要再用 this.property = ... 设置初始值。
不要为了消除报错而把属性改成 any,或把 Letter 放宽成 string,否则会失去对允许值的检查。原始材料未提供 TypeScript 版本和检查配置,因此不能据此认定所有版本都会产生相同的推断结果。
备注:内容仅供参考。
