TypeScript中派生类继承基类却无法赋值,是否违反里氏替换原则?
明明DerivedClass继承了SuperClass,但执行const obj: SuperClass = new DerivedClass()时却报错Type 'DerivedClass' is not assignable to type 'SuperClass',这是为什么?是否违反里氏替换原则?
先看第一段触发报错的代码:
class SuperClass { func(arg: (me: this) => void) { return arg } } class DerivedClass extends SuperClass { variable = this.func(() => {}); } const obj: SuperClass = new DerivedClass();
为进一步定位问题,下面这段代码同样会在赋值时报错,除非移除两个类中的protected修饰符:
abstract class SuperClass { protected abstract variable: (me: this) => void; } class DerivedClass extends SuperClass { protected variable = (me: this) => {}; } const obj: SuperClass = new DerivedClass();
核心原因:多态this类型与函数参数的逆变特性
问题的关键在于TypeScript的多态this类型和函数参数类型的逆变规则:
- 多态
this类型:在类的方法或属性中使用this作为类型时,它代表的是调用该方法/访问该属性的对象的实际类型,而非类本身的类型。比如SuperClass的func方法中,arg的类型(me: this) => void,在DerivedClass的实例上调用时,this会被解析为DerivedClass,因此func的参数类型实际是(me: DerivedClass) => void。 - 函数参数的逆变:函数类型的兼容性遵循逆变规则——如果类型A是类型B的父类,那么接受B的函数不能赋值给接受A的函数(因为A的实例不一定是B的实例,传入A会导致类型错误)。
对应第一个例子:
DerivedClass的variable属性类型是(me: DerivedClass) => void(由this.func推导而来)。- 而
SuperClass类型对应的variable属性(如果存在)应该是(me: SuperClass) => void。 - 由于
DerivedClass是SuperClass的子类,(me: DerivedClass) => void无法兼容(me: SuperClass) => void(违反逆变规则),因此DerivedClass实例不能赋值给SuperClass类型变量。
第二个例子的逻辑完全一致:
- 基类的抽象属性
variable要求类型为(me: this) => void,当基类作为类型约束时,this指向SuperClass;而派生类实现的variable类型是(me: DerivedClass) => void,同样因为逆变规则无法兼容基类的类型要求,导致赋值报错。移除protected后,TypeScript的类型检查策略会调整,不再严格校验该属性的跨类型兼容性,因此报错消失。
与里氏替换原则的关系
这并不违反里氏替换原则,反而TypeScript的报错是在维护该原则:
里氏替换原则要求派生类对象可以替换基类对象,且程序行为保持正确。如果允许把DerivedClass实例赋值给SuperClass类型变量,当外部代码调用obj.variable(new SuperClass())时,variable函数实际期望的是DerivedClass实例,传入SuperClass会导致类型不匹配,破坏程序正确性。因此TypeScript的报错是合理的,避免了潜在的类型安全问题。
为什么错误出现在赋值行而非派生类内部?
在DerivedClass定义内部,this.func(() => {})的调用完全符合类型规则:此时this是DerivedClass实例,func的参数类型被推导为(me: DerivedClass) => void,空函数() => {}可以兼容该类型,因此派生类的定义本身是合法的。只有当尝试将派生类实例赋值给基类类型时,TypeScript才会校验整个类型的兼容性,发现variable属性的类型不匹配,所以报错出现在赋值语句处。
内容的提问来源于stack exchange,提问作者Carucel

