问题
I have worked on a couple of projects using React.js. Some of them have used Flux, some Redux and some were just plain React apps utilizing Context.
I really like the way how Redux is using functional patterns. However, there is a strong chance that developers unintentionally mutate the state. When searching for a solution, there is basically just one answer - Immutable.js. To be honest, I hate this library. It totally changes the way you use JavaScript. Moreover, it has to be implemented throughout the whole application, otherwise you end up having weird errors when some objects are plain JS and some are Immutable structures. Or you start using .toJS()
, which is - again - very very bad.
Recently, a colleague of mine has suggested using TypeScript. Aside from the type safety, it has one interesting feature - you can define your own data structures, which have all their fields labeled as readonly
. Such a structure would be essentially immutable.
I am not an expert on either Immutable.js or TypeScript. However, the promise of having immutable data structures inside Redux store and without using Immutable.js seems too good to be true. Is TypeScript's readonly
a suitable replacement for Immutable.js? Or are there any hidden issues?
回答1:
While it is true that the readonly
modifier of TypeScript only exists at design type and does not affect runtime code, this is true of the entire type system. That is, nothing stops you at runtime from assigning a number to a variable of type string
. So that answer is kind of a red herring... if you get warned at design time that you're trying to mutate something marked as const
or readonly
, then that would possibly eliminate the need for extensive runtime checking.
But there is a major reason why readonly
is insufficient. There is an outstanding issue with readonly
, which is that currently (as of TS3.4), types that differ only in their readonly
attributes are mutually assignable. Which lets you easily bust through the protective readonly
shell of any property and mess with the innards:
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
This is pretty bad for readonly
. Until that gets resolved, you might find yourself agreeing with a previous issue filer who had originally named the issue "readonly modifiers are a joke" 🤡.
Even if this does get resolved, readonly
might not cover all use cases. You'd also need to walk through all interfaces and types in your libraries (or even the standard libraries) and remove methods that mutate state. So all uses of Array
would need to be changed to ReadonlyArray
and all uses of Map
would need to be changed to ReadonlyMap
, etc. Once you did this you'd have a fairly typesafe way to represent immutability. But it's a lot of work.
Anyway, hope that helps; good luck!
回答2:
The purpose of Immutable.js
is not to prevent a developer from doing an illegal mutation at compile time. It provides a convenient API to create copies of an object with some of its properties changed. The fact that you get type safeness on objects that you manage with immutable.js is basically just a side effect of using it.
Typescript is "just" a typing system. It does not implement any of the features Immutable.js
does to make copies of immutable objects. All it does, when declaring a variable as readonly
, is to check at compile time that you do not mutate it. How you design your code to handle immutability is not the scope of a typing system and you would still need a way of dealing with it.
React ensures immutability by providing a method setState
instead of mutating the state object directly. It takes care of merging the changed properties for you. But if you e.g. use redux you may want a convenient solution to handle immutability too. That is what Immutable.js
provides and typescript never will and it is independent of whether you like the api or not.
回答3:
There are two issues with this:
1) You have to use readonly
and/or things like ReadonlyArray
all the way down, which is error-prone.
2) readonly
exists solely at compile time, not runtime, unless backed by immutable data stores. Once your code is transpiled to JS your runtime code can do whatever it wants.
回答4:
Immutable js distinguishing feature compared to readonly
is structural sharing.
Here is general benefit: Imagine nested JS object that have 16 properties across multiple levels of nesting.
With readonly
the way to update a value is to copy old one, modify whatever data we want and then we have new value!
With JS
the way to update a value is to keep all the properties that did not change and only copy those that did (and their parents untill we reach a root).
Thus Immutable js saves time on update (less copying), saves memory (less copying), saves time when deciding if we need to redo some related work (e.g. we know that some leafs didn't change so their DOM do not have to be changed by React!).
As you can see readonly
is not even in the same league as Immutable js. One is mutation property, the other is efficient immutable data structure library.
回答5:
Typescript is still rough around the edges with immutability - and they still (as of Typescript 3.7) haven't fixed the issue where you can mutate readonly
objects by first assigning them to non-readonly
objects.
But the usability is still pretty good because it covers almost all other use cases.
This definition which I've found in this comment works pretty well for me:
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]> };
来源:https://stackoverflow.com/questions/55905801/can-typescripts-readonly-fully-replace-immutable-js