TypeScript编译器为何不对父类构造函数调用虚方法发出警告?
这确实是TypeScript目前的一个设计局限,编译器不会主动检测父类构造函数中调用子类虚方法的场景,哪怕这种写法会导致你遇到的Cannot read property 'push' of undefined运行时错误。咱们来一步步拆解这个问题:
问题根源
你给出的示例代码转译为JavaScript后,执行逻辑是这样的:
- 当你
new Extended([1,2,3,4,5])时,JS引擎先调用父类Base的构造函数; - 父类构造里调用了
this.initialise(),此时this指向的是Extended实例,但子类的squares属性还没完成初始化(类字段的初始化逻辑是在父类构造执行完毕后才会执行的); - 所以
initialise方法里访问this.squares时,它还是undefined,调用push自然抛出错误。
TypeScript的编译器虽然能捕获大多数undefined相关的静态错误,但这种涉及类构造执行顺序、跨类虚方法调用的场景,目前不在它的检查范围内——官方文档也没有明确禁止这种写法,社区里有人提议通过TSLint/ESLint规则来做静态检查,但还没有成为编译器的内置功能。
兼顾readonly属性的解决方案
既然你希望尽可能保留成员的readonly属性,推荐使用工厂方法模式来调整初始化流程,确保子类属性先完成初始化,再调用虚方法:
abstract class Base { constructor(protected readonly numbers: number[]) { // 移除构造函数内的initialise调用 } protected abstract initialise(): void; // 提供静态工厂方法统一创建并初始化实例 static create<T extends Base>(this: new (nums: number[]) => T, nums: number[]): T { const instance = new this(nums); instance.initialise(); return instance; } } class Extended extends Base { // 保留readonly属性和类字段初始化 private readonly squares: number[] = []; protected initialise() { this.numbers.forEach(num => this.squares.push(num * num)); } } // 通过工厂方法创建实例,而非直接new const extended = Extended.create([1,2,3,4,5]);
这个方案的核心是把初始化逻辑从父类构造函数中剥离,放到实例创建完成后执行,这样就能保证子类的squares属性已经被正确初始化为空数组,再调用initialise就不会出现undefined的问题,同时完全保留了readonly修饰符。
其他可选思路
如果你不想用工厂方法,尝试把readonly属性移到构造函数中初始化的思路其实不可行——因为JS规范要求子类构造函数必须先调用super,此时父类构造已经触发了initialise调用,子类属性还没来得及初始化,依然会报错。所以工厂方法的方案是目前最可靠的选择。
我完全理解你的困扰——本来依赖TypeScript来规避这类运行时错误,结果遇到这种底层机制导致的盲区,确实让人头疼。不过上面的工厂方法应该能很好地解决你的问题,同时满足代码的设计需求。
内容的提问来源于stack exchange,提问作者Harry de Kroon

