抽象类的实现不会强制要求只读参数。

huangapple go评论67阅读模式
英文:

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?

Playground

答案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; // 错误

这往往会使人感到困惑;也许,与其使用ReadonlyArrayArray,名称“应该”是ReadableArrayReadableWritableArray会更好...但是他们不能这样做,因为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);
    }
}

Playground link to code

huangapple
  • 本文由 发表于 2023年6月1日 15:55:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/76379781.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定