请参阅此示例:

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)

当我实现基类时,为什么不强制将props 设为只读?

Playground

推荐答案

readonly在Typescript 中的含义有一些细微的差别和复杂性.带有readonly properties的对象被视为可与属性可变的相同形状的对象相互分配.因此,在TypeScrip中执行的任何操作都不会要求或禁止将具有或不具有readonly属性的对象作为参数传递:

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

microsoft/TypeScript#13347岁时,有一个长期存在的要求以某种方式改变这种行为,但在实现之前,这就是它的方式.


当然,在您的示例代码中,您谈论的不仅仅是一些具有readonly个属性的对象;您具体地谈论的是readonly arrays, a.k.a., ReadonlyArray<T> or readonly T[]个属性.严格来说,它们是TypeScrip中可变的Array<T>T[]类型的超类型.除了具有readonly个元素之外,readonly数组还不具有像push()shift()这样的变异数组方法.所以你可以把一个可变数组赋给一个readonly,but not vice versa:

let rArr: readonly string[] = ["a"];
let mArr: string[] = ["a"];
rArr = mArr; // okay
mArr = rArr; // error

这往往会让人感到困惑;也许不是ReadonlyArrayArray,而是ReadableArrayReadableWritableArray……但他们不能真正做到这一点,因为Array已经作为JavaScript存在,而TypeScrip需要符合这个命名.因此,命名就是它的本质.

还值得注意的是,在实践中,所有ReadonlyArray<T>个类型在运行时都只是常规的读写Array<T>对象.这并不是说有一个ReadonlyArray构造函数的实例缺少push()shift().因此,即使编译器试图阻止您将readonly数组赋给可变数组,但如果您成功做到了这一点,也不太可能导致运行时错误.


有了这一点,让我们来看看所提出的问题.为了安全起见,应该只允许您用比父方法的参数大wider的参数重写方法.也就是说,函数类型的参数应为contravariant(请参见reasons for that15/2887218">Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript).但是,TypeScrip不会对方法强制执行这一规则.相反,方法被认为是bivariant个参数;除了允许您(安全地)扩大参数之外,TypeScrip还允许您narrow个参数(不安全).有reasons for that个,但这意味着TypeScrip会很乐意让你做一些不安全的事情,如下所示:

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!

这就是为什么允许World在其hello()方法中接受可变string[]的原因.子类"应该"抱怨说,要使World成为有效的Base,它不能安全地假定hello()的参数将是可变array.事实并非如此,因为两者之间存在着差异.但当然,如上所述,在实践中不会因此而出现运行时错误,因为"readonly"数组就是array.


无论如何,原来打字脚本中的所有函数参数都是双向判断的.但现在,如果您打开the --strict suite of compiler options或明确地说只打开the --strictFunctionTypes compiler option,则将相反地判断non-method个函数参数.不过,方法仍然是两种不同的.

因此,更改行为的一种可能方法是用函数值属性替换您的方法.这还有其他值得注意的影响,从methods are defined on the class prototype and not the instance开始,所以它可能不合适.但它强制实施了你所期待的限制:

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

Typescript相关问答推荐

我可以让Typescript根据已区分的联合键缩小对象值类型吗?

如何根据数据类型动态注入组件?

使用React处理Websocket数据

根据另一个属性的值来推断属性的类型

如何重构长联合类型?

TS2339:类型{}上不存在属性消息

如何使所有可选字段在打印脚本中都是必填字段,但不能多或少?

推断从其他类型派生的类型

刷新页面时,TypeScrip Redux丢失状态

无法将从服务器接收的JSON对象转换为所需的类型,因此可以分析数据

使用泛型识别对象值类型

有没有可能创建一种类型,强制在TypeScrip中返回`tyPeof`语句?

在打字脚本中对可迭代对象进行可变压缩

有没有一种方法可以提升对象的泛型级别,而对象的值是泛型函数?

更漂亮的格式数组<;T>;到T[]

通过函数传递确切的类型,但验证额外的字段

React-跨组件共享功能的最佳实践

覆盖高阶组件中的 prop 时的泛型推理

在 next.js 13.4 中为react-email-editor使用forwardRef和next/dynamic的正确方法

React 中如何比较两个对象数组并获取不同的值?