TypeScript 的 `readonly` 能完全取代 Immutable.js 吗?

Can TypeScript's `readonly` fully replace Immutable.js?

我使用 React.js 完成了几个项目。其中一些使用了 Flux,一些使用了 Redux,还有一些只是使用 Context 的普通 React 应用程序。

我真的很喜欢 Redux 使用函数式模式的方式。但是,开发人员无意中改变状态的可能性很大。在寻找解决方案时,基本上只有一个答案 - Immutable.js。老实说,我讨厌这个图书馆。它完全改变了您使用 JavaScript 的方式。此外,它必须在整个应用程序中实现,否则当某些对象是纯 JS 而某些对象是不可变结构时,您最终会遇到奇怪的错误。或者您开始​​使用 .toJS(),这再次非常糟糕。

最近,我的一位同事建议使用 TypeScript。除了类型安全之外,它还有一个有趣的特性——您可以定义自己的数据结构,其所有字段都标记为 readonly。这样的结构本质上是不可变的。

我不是 Immutable.js 或 TypeScript 方面的专家。然而,在 Redux 存储中拥有不可变数据结构而不使用 Immutable.js 的承诺似乎好得令人难以置信。 TypeScript 的 readonly 是 Immutable.js 的合适替代品吗?还是有什么隐藏的问题?

这有两个问题:

1) 必须一直往下用readonly and/or之类的ReadonlyArray,容易出错。

2) readonly 只存在于编译时,不存在于运行时,除非有不可变数据存储支持。一旦您的代码被转译为 JS,您的 runtime 代码就可以为所欲为。

Immutable.js 的目的不是为了防止开发人员在编译时进行非法修改。它提供了一个方便的 API 来创建一个对象的副本,其中某些属性已更改。使用 immutable.js 管理的对象获得类型安全这一事实基本上只是使用它的副作用。

Typescript 是 "just" 一种打字系统。它没有实现 Immutable.js 复制不可变对象的任何功能。当将变量声明为 readonly 时,它所做的只是在编译时检查您没有改变它。如何设计代码来处理不可变性不是类型系统的范围,您仍然需要一种处理它的方法。

React 通过提供一种方法 setState 而不是直接改变状态对象来确保不变性。它负责为您合并更改的属性。但是如果你例如使用 redux 你可能也需要一个方便的解决方案来处理不可变性。这就是 Immutable.js 提供的功能,打字稿永远不会提供,它与您是否喜欢 api 无关。

虽然TypeScript的readonly修饰符确实只存在于设计类型中,不影响运行时代码,但整个类型系统都是如此。也就是说,没有什么能阻止您在运行时 将数字分配给类型string 的变量。所以这个答案有点转移注意力...如果您在设计时收到警告,您正在尝试改变标记为 constreadonly 的内容,那么这可能会消除对广泛的需求运行时检查。

但是readonly不足的一个主要原因。有一个 outstanding issuereadonly,目前(从 TS3.4 开始),仅在 readonly 属性上不同的类型是可以相互分配的。这让您可以轻松突破任何 属性 的保护 readonly shell 并弄乱内部结构:

type Person = { name: string, age: number }
type ReadonlyPerson = Readonly<Person>;

const readonlyPerson: ReadonlyPerson = { name: "Peter Pan", age: 12 };
readonlyPerson.age = 40; // error, "I won't grow up!"

const writablePerson: Person = readonlyPerson; // no error?!?!
writablePerson.age = 40; // no error!  Get a job, Peter.

console.log(readonlyPerson.age); // 40

这对 readonly 来说非常糟糕。在这个问题得到解决之前,您可能会发现自己同意以前的问题提交者 originally named the issue "readonly modifiers are a joke" .

即使这个问题得到解决,readonly 也可能无法涵盖所有​​用例。您还需要遍历库(甚至标准库)中的所有接口和类型,并删除改变状态的方法。所以 Array 的所有用途都需要更改为 ReadonlyArrayMap 的所有用途都需要更改为 ReadonlyMap,等等。一旦你这样做了,你会有一种相当类型安全的方式来表示不变性。但这是很多工作。

总之,希望对您有所帮助;祝你好运!

readonly相比,不可变js的显着特征是structural sharing

一般福利如下: 想象一下嵌套的 JS 对象在多层嵌套中具有 16 个属性。

使用readonly更新值的方法是复制旧值,修改我们想要的任何数据,然后我们有新值!

使用 JS 更新值的方法是保留所有未更改的属性,仅复制更改的属性(以及它们的父项,直到我们到达根)。

因此 Immutable js 节省了更新时间(更少的复制),节省了内存(更少的复制),在决定我们是否需要重做一些相关工作时节省了时间(例如,我们知道一些叶子没有改变所以他们的 DOM 不必被 React 改变!)。

如您所见,readonly 甚至与 Immutable js 不在同一个级别。一个是变异属性,一个是高效的不可变数据结构库

Typescript 在不可变性的边缘仍然很粗糙 - 而且它们仍然(从 Typescript 3.7 开始)没有解决你可以通过首先将它们分配给非 [=11 来改变 readonly 对象的问题=] 个对象。

但是可用性还是不错的,因为它涵盖了几乎所有其他用例。

我在 this comment 中找到的这个定义对我来说非常适用:

type ImmutablePrimitive = undefined | null | boolean | string | number | Function;

export type Immutable<T> =
  T extends ImmutablePrimitive ? T :
    T extends Array<infer U> ? ImmutableArray<U> :
      T extends Map<infer K, infer V> ? ImmutableMap<K, V> :
        T extends Set<infer M> ? ImmutableSet<M> : ImmutableObject<T>;

export type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
export type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
export type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
export type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };