简体   繁体   中英

Immutable JS - how to preserve Type and get immutability when converting a deeply nested JS object?

Working on a React, Redux + Typescript project, I am trying to add Immutable JS to the stack.

I started with working on a large nested object that could really use being safer as an immutable data structure.

import { Record, fromJS } from "immutable";

const obj = {
  name: "werwr",
  overview: {
    seasons: {
      2017: [{ period: 1, rates: 2 }]
    }
  }
};
// -- Using fromJS
const objJS = fromJS(obj);
const nObj = objJS.getIn(["overview", "seasons", "2017"]);
console.log(nObj); // I get an immutable list cool!

// -- Using Record, infer the type
const objRecord = Record(obj)();
const nRec = objRecord.getIn(["overview", "seasons", "2017"]);
console.log(nRec); // but I get a JS array

// -- Using both
const makeRec = Record(objJS);
const bothRecord = makeRec({ name: "name" });
console.log(bothRecord); // fails

Runnable code in codesandbox: https://codesandbox.io/s/naughty-panini-9bpgn?file=/src/index.ts

  1. using fromJS. The conversion works well and deep but I lose all type information.
  2. using a Record. It keeps track of the type but nested arrays are still mutable.
  3. passing the converted object into a Record and manually add the type but I ran into an error: Cannot read property 'get' of undefined

Whats the proper way to convert such an object to a fully immutable data structure while not loosing the type? Thanks!

You can use classes to construct deep structures.

interface IRole {
  name: string;
  related: IRole[];
}

const roleRecord = Record({
  name: '',
  related: List<Role>(),
});

class Role extends roleRecord {
  name: string;
  related: List<Role>;
  constructor(config: IRole) {
    super(Object.assign({}, config, {
      related: config.related && List(config.related.map(r => new Role(r))),
    }));
  }
}

const myRole = new Role({
  name: 'President',
  related: [
    {name: 'VP', 
    related:[
      {name: 'AVP', 
      related: []}
    ]}
  ]});

With this type of structure, myRole will be all nested Role classes.

NOTE: I will add a bit of caution, we have been using this structure in a production application for almost 4 years now (angular, typescript, redux), and I added the immutablejs for safety from mutated actions and stores. If I had to do it over, the strict immutable store and actions that comes with NGRX would be my choice. Immutablejs is great at what it does, but the complexity it adds to the app is a trade off (Especially for onboarding new/greener coders).

Record is a factory for Record-Factories. As such, the argument should be an object template (aka default values), not actual data! ( see docs ).

const MyRecord = Record({
  name: "werwr",
  overview: null
});

const instance = MyRecord(somedata);

As you already noticed, the Record factory will not transform data to immutable. If you want to do that, you have to either do it manually with Maps and Lists , fromJS or the constructor of records.

The last approach is a bit weird, because then your record factory suddendly becomes a class:

const SeasonRecord = Record({
  period: null, rates: null
})

class MyRecord extends Record({
    name: "default_name",
    seasons: Map()
}, 'MyRecord') {
    constructor(values = {}, name) {
        if(values.seasons) {
          // straight forward Map of seasons: 
          // values = fromJS(values);

          // Map of sub-records
          values.seasons = Object.entries(values.seasons).reduce(
            (acc, [year, season]) => { 
              acc[year] = SeasonRecord(season);
              return acc;
            }, {});
          values.seasons = Map(values.seasons);
        }
        super(values, name);
    }
}

const x = new MyRecord({
  seasons: {
      2017: { period: 1, rates: 2 }
    }
})
console.log('period of 2017', x.seasons.get('2017').period)

I strongly suggest to not use unecessarily nest objects (record -> overview -> season) as it makes everything more complicated (and if you use large amounts of records, it might impact performance).

My general recommendation for Records is to keep them as flat as possible. The shown nesting of records allows to use the property access syntax instead of get , but is too tendious most of the time. Simply doing fromJS() for the values of a record and then use getIn is easier.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM