英文:
Implementation of abstract class does not force readonly arguments
问题
请查看此示例:
abstract class Base {
abstract hello(
ids: readonly string[],
): void;
}
class World extends Base {
hello(ids: string[]): void {
ids.push('hello world')
console.log(ids);
}
}
const ids: readonly string[] = ['id'];
const t:Base = new World();
t.hello(ids)
当我实现基类时,为什么我不被强制要求将属性设置为只读?
英文:
See this example:
abstract class Base {
abstract hello(
ids: readonly string[],
): void;
}
class World extends Base {
hello(ids: string[]): void {
ids.push('hello world')
console.log(ids);
}
}
const ids: readonly string[] = ['id'];
const t:Base = new World();
t.hello(ids)
When I implement the base class why am I not forced to make the prop readonly?
答案1
得分: 1
在TypeScript中,关于readonly
的含义存在一些微妙和复杂的情况。拥有readonly
属性的对象被认为与具有可变属性的相同形状的对象可以互相赋值。因此,在TypeScript中,无论你做什么,都不会要求或禁止带有或不带有readonly
属性的对象作为参数传递:
const xr: { readonly a: string } = { a: "" };
const xm: { a: string } = { a: "" };
let r = xr; r = xm; // 可行
let m = xm; m = xr; // 可行
function foo(x: { readonly a: string }) { }
foo(xr); // 可行
foo(xm); // 仍然可行
function bar(x: { a: string }) { }
bar(xm); // 可行
bar(xr); // 仍然可行
在microsoft/TypeScript#13347中一直存在着一个长期请求,以某种方式更改这种行为,但在实施之前和除非实施,它将保持现状。
当然,你在示例代码中并不仅仅是在谈论带有readonly
属性的一些对象;你特别谈论的是readonly
数组,也就是ReadonlyArray<T>
或readonly T[]
。在TypeScript中,这些类型是可变Array<T>
或T[]
类型的严格超类型。除了具有readonly
元素之外,readonly
数组不具备像push()
或shift()
这样的可变数组方法。因此,你可以将可变数组赋值给readonly
数组,但反之则不行:
let rArr: readonly string[] = ["a"];
let mArr: string[] = ["a"];
rArr = mArr; // 可行
mArr = rArr; // 错误
这往往会使人感到困惑;也许,与其使用ReadonlyArray
和Array
,名称“应该”是ReadableArray
和ReadableWritableArray
会更好...但是他们不能这样做,因为Array
已经存在于JavaScript中,而TypeScript需要遵循该命名规范。所以名称就是现在的样子。
值得注意的是,在实际中,所有的ReadonlyArray<T>
类型在运行时都只是常规的可读写Array<T>
对象。并不存在一个ReadonlyArray
的构造函数,其实例缺乏push()
和shift()
等方法。因此,尽管编译器试图阻止你将readonly
数组赋值给可变数组,但如果你设法这样做,通常不会导致运行时错误。
有了这些信息,让我们来看一下提出的问题。为了安全起见,你只能允许覆盖具有比父方法更广泛参数的方法。也就是说,函数类型的参数应该是逆变的(参见https://stackoverflow.com/q/66410115/2887218)。但是TypeScript并不强制执行这个规则,对于方法,它将参数视为双变的;除了让你扩展参数(安全地),TypeScript还允许你缩小参数(不安全)。有一些原因导致了这种情况,但这意味着TypeScript将乐意让你做一些不安全的事情,如下所示:
class X {
method1(x: string) { }
method2(x: string) { }
}
class Y extends X {
method1(x: string | number) { // 安全
console.log(typeof x === "string" ? x.toUpperCase() : x.toFixed(1))
}
method2(x: "a" | "b") { // 不安全
console.log({ a: "hello", b: "goodbye" }[x].toUpperCase())
}
}
const x: X = new Y();
x.method1("abc"); // "ABC"
x.method2("abc"); // ⚡ 运行时错误!
所以这就是为什么World
在其hello()
方法中允许接受可变的string[]
。子类“应该”抱怨,因为要使World
成为有效的Base
,它不能安全地假设hello()
的参数将是可变数组。但是由于双变性,它没有这样做。但是,正如上面提到的,实际上不会因此而导致运行时错误,因为“readonly
”数组只是数组而已。
无论如何,最初在TypeScript中,所有函数参数都以双变方式进行检查。但现在,如果你打开编译器选项的--strict
套件或特别只打开--strictFunctionTypes
编译器选项,那么非方法函数参数将会以逆变方式进行检查。但方法仍然是双变的。
因此,要改变这种行为的一种可能方法是用一个函数值属性替换你的方法。这会产生其他明显的效果,因为方法是在类prototype
上定义的,而不是在实例上定义的,所以这可能不太合适。但它确实强制
英文:
There's some nuance and complication around what readonly
means in TypeScript. Objects with readonly
properties are considered mutually assignable with objects of the same shape whose properties are mutable. So nothing you do in TypeScript will either require or prohibit objects with or without readonly
properties from being passed as arguments:
const xr: { readonly a: string } = { a: "" };
const xm: { a: string } = { a: "" };
let r = xr; r = xm; // okay
let m = xm; m = xr; // okay
function foo(x: { readonly a: string }) { }
foo(xr); // okay
foo(xm); // still okay
function bar(x: { a: string }) { }
bar(xm); // okay
bar(xr); // still okay
There is a longstanding request at microsoft/TypeScript#13347 to change this behavior in some way, but until and unless that gets implemented, this is the way it is.
Of course you're not just talking about some objects with readonly
properties in your example code; you're specifically talking about readonly
arrays, a.k.a., ReadonlyArray<T>
or readonly T[]
. These are strictly supertypes of mutable Array<T>
or T[]
types in TypeScript. In addition to having readonly
elements, a readonly
array is not known to have the mutating array methods like push()
or shift()
. So you can assign a mutable array to a readonly
one, but not vice versa:
let rArr: readonly string[] = ["a"];
let mArr: string[] = ["a"];
rArr = mArr; // okay
mArr = rArr; // error
This tends to confuse people; perhaps instead of ReadonlyArray
and Array
, the names "should" be ReadableArray
and ReadableWritableArray
... but they couldn't really do that because Array
already exists as JavaScript and TypeScript needs to conform to that naming. So the naming is what it is.
It also worth noting that in practice, all ReadonlyArray<T>
types are just regular read-write Array<T>
objects at runtime. It's not like there's a ReadonlyArray
constructor whose instances lack push()
and shift()
. So even though the compiler tries to prevent you from assigning a readonly
array to a mutable array, it's not likely to cause a runtime error if you manage to do it.
Armed with that, let's look at the question as asked. In order to be safe, you should only be allowed to override methods with parameters that are wider than the parent method's parameters. That is, function types should be contravariant in their parameters (see https://stackoverflow.com/q/66410115/2887218 ). But TypeScript doesn't enforce this rule for methods. Instead, methods are considered to be bivariant in their parameters; in addition to letting you widen the parameters (safely), TypeScript also lets you narrow the parameters (unsafely). There are reasons for that, but it means TypeScript will happily let you do some unsafe things, as shown:
class X {
method1(x: string) { }
method2(x: string) { }
}
class Y extends X {
method1(x: string | number) { // safe
console.log(typeof x === "string" ? x.toUpperCase() : x.toFixed(1))
}
method2(x: "a" | "b") { // unsafe
console.log({ a: "hello", b: "goodbye" }[x].toUpperCase())
}
}
const x: X = new Y();
x.method1("abc"); // "ABC"
x.method2("abc"); // 💥 RUNTIME ERROR!
So that's why World
is allowed to accept a mutable string[]
in its hello()
method. The subclass "should" complain that for World
to be a valid Base
it cannot safely assume that the arguments to hello()
will be mutable arrays. It doesn't, because of bivariance. But of course, as mentioned above, in practice there will be no runtime error because of this, since "readonly
" arrays are just arrays.
Anyway, originally all function parameters in TypeScript were checked bivariantly. But now if you turn on the --strict
suite of compiler options or specifically just the --strictFunctionTypes
compiler option, then non-method function parameters will be checked contravariantly. Methods are still bivariant though.
So one possible approach you can take to change the behavior is to replace your method with a function-valued property. This has other noticeable effects, since methods are defined on the class prototype
and not the instance, so it might not be appropriate. But it enforces the restriction you were expecting:
abstract class Base {
abstract hello(ids: readonly string[]): void;
abstract goodbye: (ids: readonly string[]) => void;
}
class World extends Base {
hello(ids: string[]): void { // okay
ids.push('hello world')
console.log(ids);
}
goodbye = (ids: string[]) => { // error as desired
ids.push('hello world')
console.log(ids);
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论