4

I am trying to introduce immutable.js to an Angular2 project and I am having trouble using typescript classes with immutable.js.

I am trying to deep convert a mutable object coming from a class into an immutable one using Immutable.fromJS function. Unfortunately, although fromJS works fine on json objects, it does nothing (or throws when trying to call .toJS() to the generated object) if an object coming from a class is given.

class Person {
  public firstName: string;
}

let p = new Person();
p.firstName = 'Mike';
console.log(p);

let immutablePersonFail = Immutable.fromJS(p);
console.log(Immutable.Map.isMap(immutablePersonFail)); // returns false

let immutablePersonSuccess = Immutable.fromJS({firstName: 'Nick'});
console.log(Immutable.Map.isMap(immutablePersonSuccess)); // returns true

here is jsbin demonstrating the issue: https://jsbin.com/yefeha/edit?js,console

masimplo
  • 3,674
  • 2
  • 30
  • 47
  • This is probably because `p` is a class instance and not a "plain object", check [this thread I just answered recently](http://stackoverflow.com/questions/37300338/how-can-i-convert-a-typescript-object-to-a-plain-object) – Nitzan Tomer May 18 '16 at 13:40

3 Answers3

1

Try this:

let immutablePersonFail = Immutable.fromJS(Object.assign({}, p));
console.log(Immutable.Map.isMap(immutablePersonFail));

More on that in the other thread I referenced in my comment.


Edit

This is one option:

class Base {
    toObject(): any {
        return Object.assign({}, this);
    }
}

class Job extends Base {
    public jobTitle: string;
}

class Person extends Base {
    public firstName: string;
    public jobs: Job[];

    toObject(): any {
        let obj = super.toObject();

        obj.jobs = obj.jobs.map(job => job.toObject());

        return obj;
    }
}

The other option is to differentiate between the data and functionality:

interface JobData {
    title: string;
}

class Job {
    private data: JobData;

    getTitle(): string {
        return this.data.title;
    }

    getData(): JobData {
        return Immutable.fromJS(this.data);
    }
}

interface PersonData {
    firstName: string;
    lastName: string;
    jobs: JobData[];
}

class Person {
    private data: PersonData;
    public jobs: Job[];

    getData(): JobData {
        return Immutable.fromJS(this.data);
    }
}

As for complexity, I can't say, it depends on what your business logic is, and how you implement it.

Community
  • 1
  • 1
Nitzan Tomer
  • 155,636
  • 47
  • 315
  • 299
  • Object.assign does seem to solve the problem when the class if flat like above. But two questions. First what happens with nested structures (updated my jsbin to show the issue) since Object.assign only performs shallow copy. And second, is using Object.assign a recommended approach when dealing with ES6 classes and Immutable.js as it would put an extra complexity every time when moving from mutable to immutable constructs. – masimplo May 18 '16 at 14:12
  • check my revised answer – Nitzan Tomer May 18 '16 at 14:43
1

As you have noticed, converting a class to a immutable object requires changing the class to be in a different form—regular js object. To me the best solution here is to not to use a class in the first place. It would be less painful to use a regular object and then use patterns that work well with those.

interface Person {
    firstName: string;
    phoneNumbers: string[];
}

const p: Person = { firstName: "Mike", phoneNumbers: [] };   
const immutablePersonFail = Immutable.fromJS(p);
console.log(Immutable.Map.isMap(immutablePersonFail)); // true

If you were originally using a class in order to help initialize the object, then you can use a function or factory class method that helps create the regular js object:

function createPerson(firstName: string) {
    const person: Person = {
        firstName: firstName,
        phoneNumbers: []
    };
    return Immutable.fromJS(person);
}

If you were originally using a class to get information from the data, then you can use a separate class for that:

class PersonHandler {
    constructor(private person: Person) { // or typed as whatever fromJS returns
    }

    getFirstPhoneNumber() {
        return this.person.phoneNumbers[0];
    }
}
David Sherret
  • 101,669
  • 28
  • 188
  • 178
  • 3
    Thank you for your detailed answer David. To tell you the truth I thought of your approach a good idea and went ahead to implement it in a branch on my code. Now that all my tests pass again after refactoring a great amount of my code to work without classes, I can safely say, that it feels a step backward not to use classes. I feel like some of the "automation" that typescript provides has gone away. Not 100% sure, but probably will not go down this road. Cool answer though! – masimplo May 20 '16 at 09:14
0

I had a similar problem and worked it out by creating immutable structures from typescript classes that would also convert the children/attributes of those to immutable objects.

For example:

class Car {
  make: string;
  model: string;
  wheels: Wheel[]; // Is a collection of wheel objects (new Wheel() ..)

  toImmutable() {
   return Immutable.fromJS({
     ...omit(this, 'wheels'), // Lodash omit function, pick all other attributes except the wheels
     wheels: this.wheels.map((w: Wheel) => w.toImmutable())
   });
  }
 }

class Wheel {
  size: number;
  bolts: number;

  toImmutable() {
    return Immutable.fromJS(Object.assign({}, this));
  }
}

// Later, something like:

const car = new Car();
car.addWheel(new Wheel());
car.addWheel(new Wheel());

// Convert to immutable object
const immutableCar = car.toImmutable();

immutableCar now yields an immutable map that also has wheels as immutable maps.