TypeScript 的 `readonly` 可以完全替代 Immutable.js 吗?

Posted

技术标签:

【中文标题】TypeScript 的 `readonly` 可以完全替代 Immutable.js 吗?【英文标题】:Can TypeScript's `readonly` fully replace Immutable.js? 【发布时间】:2019-09-18 05:19:53 【问题描述】:

我曾使用 React.js 参与过几个项目。其中一些使用了 Flux,一些 Redux 和一些只是使用 Context 的普通 React 应用程序。

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

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

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

【问题讨论】:

有趣的问题,我同意您在 Immutable.js 上的 cmets,文档很糟糕,而且不是最直观的。但是,我认为 TS(只读)不会成为 Immutable.js 的简单替换。 Object.freeze() 呢? 你会喜欢这对:Immer JS + TypeScript 【参考方案1】:

虽然 TypeScript 的 readonly 修饰符确实只存在于设计类型中并且不会影响运行时代码,但整个类型系统都是如此。也就是说,没有什么能阻止您在运行时将数字分配给string 类型的变量。因此,这个答案有点牵强... /p>

但是readonly不够用有一个主要原因。有一个 outstanding issue 和 readonly,这是目前(从 TS3.4 开始),仅在 readonly 属性上不同的类型可以相互分配。这让您可以轻松地突破任何财产的保护性readonly 外壳并弄乱内脏:

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 也可能无法涵盖所有​​用例。您还需要遍历库(甚至标准库)中的所有接口和类型,并删除改变状态的方法。所以Array 的所有用途都需要更改为ReadonlyArrayMap 的所有用途都需要更改为ReadonlyMap,等等。一旦你这样做了,你就会有一种相当类型安全的方式来表示不变性.但工作量很大。

无论如何,希望对您有所帮助;祝你好运!

【讨论】:

readonly 永远不会涵盖所有用例,仅仅因为它不处理运行时不变性这一事实。 Immutable.js 主要不是正确类型检查的替代品,而是一个方便地处理必须是不可变对象的更改的 api。 Typescript 不提供,也永远不会提供。【参考方案2】:

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

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

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

【讨论】:

我知道。不过,这里似乎有暗示。如果编译器不允许您编写会改变状态的代码,那么您就不会编写会改变状态的代码。因此,您的代码库将是“无突变”......或者,“不变”......基本上,您将实现不改变状态的目标。 @Storm 你问打字稿是否可以替换immutable.js,答案显然是否定的。 Typescript 确保编译时类型安全。 Immutable.js 确保运行时不变性。这是两个不同的东西。 我明白你在说什么。如果您的代码库是“无突变”的,我无法想象会发生运行时突变的场景。 @Storm 但是 immutable.js 的工作也不是确保而不是使用打字稿。事实上,您可以同时使用两者。 Immuable.js 帮助您处理在运行时必须不可变的对象的修改。如果你不使用它,你仍然必须提供一个自己的实现来处理它。【参考方案3】:

这有两个问题:

1) 您必须一直使用readonly 和/或ReadonlyArray 之类的东西,这很容易出错。

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

【讨论】:

Ad 1) 仅拥有您的状态模型readonly 还不够吗?广告 2)查看我对@trixn 的回答的评论。 @Storm "如果你的代码库是 '无突变'" 将责任推回到开发者身上。开发人员容易出错。并不是所有的突变都像foo[1] = "hi" 一样简单,因为数组索引可能是动态的或错误的。 TypeScript(或多或少)确保您的代码在 编译 时是正确的:由于开发人员、数据或用户错误而无法在 runtime 中存活的代码是没那么有用。如果您的代码非常简单以至于编译时检查就足够了,那么可以肯定的是,您不需要 JS 级别的不变性,而单独的 TS 可能就足够了:但它不能处理运行时不变性。【参考方案4】:

readonly相比,不可变js的区别是structural sharing

以下是一般好处: 想象一下嵌套的 JS 对象,它具有跨多个嵌套级别的 16 个属性。

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

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

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

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

【讨论】:

【参考方案5】:

Typescript 的边缘仍然很粗糙,具有不变性 - 它们仍然(从 Typescript 3.7 开始)没有解决您可以通过首先将 readonly 对象分配给非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]> ;

【讨论】:

以上是关于TypeScript 的 `readonly` 可以完全替代 Immutable.js 吗?的主要内容,如果未能解决你的问题,请参考以下文章

TypeScript 的 `readonly` 可以完全替代 Immutable.js 吗?

从0开始的TypeScriptの八:类

从0开始的TypeScriptの八:类

将setState与typescript一起使用

从0开始的 TypeScriptの十四:内置工具类型

从0开始的 TypeScriptの十四:内置工具类型