下面的代码(在Playground here处)在变量obj处给出错误Type 'DerivedClass' is not assignable to type 'SuperClass'.这怎么可能,当DerivedClass extends SuperClass?这与Liskov替代原理相矛盾吗?

class SuperClass
{
    func(arg: (me: this) => void) {
        return arg
    }
}

class DerivedClass extends SuperClass
{
    variable = this.func(() => {});
}

const obj: SuperClass = new DerivedClass();

为了进一步隔离"问题",出于某种原因,following code个错误,除非在两个类中删除protected:

abstract class SuperClass
{
    protected abstract variable: (me: this) => void;
}

class DerivedClass extends SuperClass
{
    protected variable = (me: this) => {};
}

const obj: SuperClass = new DerivedClass();

Liskov替换原理有很多定义,但通常假设派生类型的对象可以像/假装是超类型.如果继承有问题,我希望DerivedClass的定义中会出现错误,而不是最后一行代码.

推荐答案

microsoft/TypeScript#39375.

TypeScript的行为符合预期;当你在一个逆变或不变的位置使用polymorphic this type时(参见Difference between Variance, Covariance, Contravariance, Bivariance and Invariance in TypeScript),那么子类将不再被认为是它的基类的子类型.

这不是违反the Liskov Substitution Prinicple,因为多态this的行为就像一个implicit generic类型参数,即recursively bounded (also known as F-bounded). 参见microsoft/TypeScript#4910,pull request实现多态this.特别是,它说"多态this类型是通过 for each 类和接口提供一个隐含的类型参数来实现的,该参数被约束到包含类型本身".


你可以用显式的F—界泛型模拟this的行为. 例如:

class Foo<T extends Foo<T>> {
    m = (x: T) => { }
}
interface FooSelf extends Foo<FooSelf> { }

class Bar<T extends Bar<T>> extends Foo<T> {
    a = "z";
    m = (x: T) => {
        console.log(x.a.toUpperCase());
    }
}
interface BarSelf extends Bar<BarSelf> { }

这里,FooBar在类型参数T中都是泛型的,类型参数T对于自身的函数是constrained.

它们都有一个函数值为m的属性,其参数类型为T.因为Bar中的T被约束为Bar<T>,那么在m中,你可以确定x具有Bar的所有属性,包括a属性. 但是在Foom里面你不能确定,因为Foo中的T只被限制在Foo<T>.

因此,虽然Bar<T>Foo<T>的一个适当的子类型,但你不能对Bar<Bar<T>>Foo<Foo<T>>,或Bar<Bar<Bar<T>>>Foo<Foo<Foo<T>>>,或它们的递归极限:FooSelfBarSelf.

因此,下面的任务是故意的错误.

const b: BarSelf = new Bar();
const f: FooSelf = b; // error, type 'BarSelf' is not assignable to type 'FooSelf'.

如果允许你在需要FooSelf的情况下替换BarSelf,那么每当你在非Bar参数上调用m()方法时,都会出现运行时错误:

f.m(new Foo()); // runtime error

请注意,所有这些都不会被视为LSP违规.


现在考虑上面的版本,它使用多态this而不是显式泛型:

class Foo {
    m = (x: this) => { }
}

class Bar extends Foo {
    a = "z";
    m = (x: this) => {
        console.log(x.a.toUpperCase())
    }
}

const b: Bar = new Bar();
const f: Foo = b; // error, Type 'Bar' is not assignable to type 'Foo'.
f.m(new Foo()); // runtime error

基本上是一样的.泛型是隐式的,但它仍然存在. 一旦您在非协变位置使用多态this类型,您就设置了一种情况,即类或接口层次 struct 不再是类型层次 struct . 但这并不是LSP违规,只是一个非常奇怪的情况,其中一个类依赖于自身的方式,使得通常的规则class X extends Y {⋯}interface X extends Y {⋯}意味着X extends Y不再成立.


但是,即使这不被认为是类型系统错误,这并不意味着TypeScript的每个行为都是正确的. TypeScript是full种可替换性侵犯. TypeScript的类型系统既不是sound,也不是完整的.这种违反本身并不可取,但为了支持惯用的JavaScript,它们基本上是不可避免的,这是"对产生可用的声音类型系统极其不利"(see this comment on microsoft/TypeScript#9825).TypeScript有一些故意的特性,其中类型X被认为是另一个类型Y的子类型,但如果你实际上用类型X的值替换Y,那么你可能会得到运行时错误.例如:

const a: { x: string } = { x: "abc" }; // allowed
const b: { x: string | number } = a; // considered substitutable, but
b.x = 2; // you can do this
a.x.toUpperCase(); // RUNTIME ERROR! LSP VIOLATION

相反地,有些特性是,类型Xnot,被认为是另一个类型Y的子类型,即使如果你用类型X的值替换Y,永远不会有运行时问题:

const c: { x: string | number } = { x: "abc" };
const d: { x: string } | { x: number } = c; // error, but why?
//    ~ <-- Type '{ x: string | number; }' is not assignable to 
//       type '{ x: string; } | { x: number; }'.

因此,即使您发现子类型和可替换性不正确匹配的东西,也不一定是TypeScript中的bug.

Playground link to code

Typescript相关问答推荐

如何在类型脚本中声明对象其键名称不同但类型相同?

TypeScript将键传递给新对象错误

当使用`type B = A`时,B的类型显示为A.为什么当`type B = A时显示为`any `|用A代替?

将值添加到具有不同类型的对象的元素

数组文字中对象的从键开始枚举

如何在TypeScrip中使用嵌套对象中的类型映射?

如何在TypeScript中将一个元组映射(转换)到另一个元组?

来自类型脚本中可选对象的关联联合类型

TypeScrip-将<;A、B、C和>之一转换为联合A|B|C

为什么有条件渲染会导致material 自动完成中出现奇怪的动画?

使用Redux Saga操作通道对操作进行排序不起作用

AngularJS服务不会解析传入的Json响应到管道中的模型

复选框Angular 在Ngfor中的其他块中重复

使用ngfor循环时,数据未绑定到表

使用&reaction测试库&如何使用提供程序包装我的所有测试组件

如何从抽象类的静态方法创建子类的实例?

Route.ts文件中未导出HTTP方法

如何知道使用TypeScript时是否在临时工作流内运行

具有枚举泛型类型的 Typescript 对象数组

如何在没有空接口的情况下实现求和类型功能?