见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> { }
这里,Foo
和Bar
在类型参数T
中都是泛型的,类型参数T
对于自身的函数是constrained.
它们都有一个函数值为m
的属性,其参数类型为T
.因为Bar
中的T
被约束为Bar<T>
,那么在m
中,你可以确定x
具有Bar
的所有属性,包括a
属性. 但是在Foo
的m
里面你不能确定,因为Foo
中的T
只被限制在Foo<T>
.
因此,虽然Bar<T>
是Foo<T>
的一个适当的子类型,但你不能对Bar<Bar<T>>
和Foo<Foo<T>>
,或Bar<Bar<Bar<T>>>
和Foo<Foo<Foo<T>>>
,或它们的递归极限:FooSelf
和BarSelf
.
因此,下面的任务是故意的错误.
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
相反地,有些特性是,类型X
是not,被认为是另一个类型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