Last Updated: December 30, 2020
·
35.3K
· nadavsinai

Using immutable.js in Typescript

Immutablity <vs> type safety

My latest project has blessed me with a chance to work with Angular2 and Typescript, this is quite a change from writing in babel transpiled ES6 as I'm used to.
The types provide strong tools - Webstorm 12 in my case - seems to 'know" what I'm doing and offers great help by suggesting autocomplete and marking my mistakes.
Current frontend data flow architecture's such as Redux and @ngrx/store benefit greatly for the use of Immutable data structure's
However this breaks Typescript's ability to suggest property names.

the Clash of titans

let examine:

a simple hash declared here


const myHash = {
           property:"value"
}

will make the IDE suggest the internal property when writing the object's reference (even without a type)
so when you write myHash.

you will get a dropdown menu in which "property" will be the first option

you can ofcourse get even better results

interface myHash{
property:string
}
const myHash<myHash> = {property:"value"}

//in use 
myHash.property = 5; 
//the IDE will mark the above line as error (and indeed the TS compiler will error too)

But when you introduce immutability the use of = for setting values is limited to assigning new instances


const myHash = Immutable.fromJS({property:"value"})


myHash.property = 5 // Error : immutable can not be changed
//even worse - myHash does not have autocomplete for .property.... it does not have this property
console.log(myHash.property) // undefined

that is because Immutable.fromJS creates an immutable.Map when using it with a source POJO argument (plain old javascript object)

the right way to use it is this:

myHash.get('property'); // value
let newValueHash = myHash.set('property','newValue');

using the property name as string argument kills the help of typescript and all the new IDE tools based on that.

Record to the rescue

A much better behaviour can come from using Immutable.Record :

const myHashRecord = new Immutable.Record({property:'defaultValue'})
let myHash = new myHashRecord();
console.log(myHash.property) // defaultValue
//notice that the property can be accessed like in POJO 
//but for setting new value we need to use it like before in the Map's case
myHash.property = 555; //Error
let newHashValue = myHash.set('property'','newValue') //works

so this gives us help with regards to autocomplete but does not :
1. help us with the need to write the property names as strings in the case of setting new properties
2. protects us from using the wrong type when setting new value

I found a combination between a Immutable Record and a Typescript Class can at least give us the 2nd point done, it does make us write our property list twice but I find it's worth the keystrokes time.

let demoRecord = Immutable.Record({
                     property:'defaultValue',
                     index:0,
                     works:true,
                     valueList:Immutable.List([])
});

export class Demo extends demoRecord {
             property:string;
             index:number;
             truth:boolean;
             valueList:Immutable.List<YourMoreComplexTypeHere>
}

let demo = new Demo();

now when writing demo. the IDE will suggest to you your properties as well as the Record's prototype methods such as .set, .toJS etc.
but the IDE will also know that the "index" property is a number and that "truth" is a boolean.
Of course you can use more complex types based on your type and interface definitions.

More thoughts

the Immutable.js library is coming from Facebook, Typescript is Microsoft technology, we build on the shoulders of giants here, I wonder why we have to make such tricks to make those two play together nicely, and this is still not perfect.
there is an issue on this subject in Facebooks repo, I remember seeing one also in Microsoft's repo but could not find it today.

There are also other Immutable libraries as some of the comments on that thread point out, immutability can be achived via Object.defineProperty by writting getters only, or one can use typings code generatos such as ts-immutable but I suggest the future must be simpler for this case
I'll be very happy to hear your thoughts on the matter.

6 Responses
Add your response

Nice, thanks for sharing that, duplicating all members declarations is indeed a price to pay, but it might be a acceptable sacrifice for the MS and FB gods. (-:

over 1 year ago ·

We are using the exact same technology stack and ran into the exact same issue. We decided on keeping the compile time checks and ~typesafety instead of keeping immutable. To ensure we still had immutable data structures we wrote a middleware that calls freeze on the store state after every action. This middleware is only configured in development builds. Then in the reducer, we first copy the state and then update the properties directly which keeps the typing intact. It takes more lines of code than using immutable but you get to keep all the nice refactoring capabilities of typescript.

over 1 year ago ·

Hi Nadav,
Thanks for sharing this helpfull article. It looks very promissing and we may use your approach in our projects as well.
I was wondering. Do you still use this approach or did you find a better way to maintain type safety and immutability?
Hope to hear from you.

over 1 year ago ·

Yep, still using it today...
I also suggest you have a look (at this)[https://github.com/rangle/typed-immutable-record]

over 1 year ago ·

Thanks for your prompt reply! We will consider that one too.

over 1 year ago ·

Hi Nadav,

One alternative is https://github.com/engineforce/ImmutableAssign, which is a lightweight immutable helper that allows you to continue working with POJO (Plain Old JavaScript Object), and supports full TypeScript type checking.

over 1 year ago ·