1

Assume I have BehaviorSubject with User interface:

interface User {
  firstName: string;
  lastName: string;
}

let user: User = {
  firstName: 'Cuong',
  lastName: 'Le',
};

let bs = new BehaviorSubject<User>(user);

There are two subsciptions, sub1 tried to change the first name. Sub2 subscribes later and user object has first name changed also as the sub1 did change it before:

let sub1 = bs.subscribe((u) => {
  u.firstName = 'An';

  console.log(u);
});

let sub2 = bs.subscribe((u) => {
  console.log(u);
});

it's hard to debug when this case happens in big Angular application. How we make the value immutable when subscribing?

I am looking deep immutable solution to prevent code somewhere else to change the data instead of the shadow one

cuongle
  • 74,024
  • 28
  • 151
  • 206
  • This surprised me - but is indeed possible. More here: https://levelup.gitconnected.com/master-rxjs-data-stores-in-services-c1f553e5d48b – MikeOne Nov 12 '21 at 17:55

3 Answers3

1

Using this answer and wrapping your type in this Immutable utility type should solve your problem. Unlike the readonly keyword or ReadOnly utility type, the Immutable interface in this answer will recursively makes all properties read only.

interface User {
  firstName: string;
  lastName: string;
  address: { 
    street: string;
  } 
}

let user: User = {
  firstName: 'Cuong',
  lastName: 'Le',
  address: { 
    street: '123 Sesame St.'
  }
};

let bs = new BehaviorSubject<Immutable<User>>(user);

let sub1 = bs.subscribe((u) => {
  u.address.street = '456'; //<-- Cannot assign to 'street' because it is a read-only property.
  console.log(u);
});
Daniel Gimenez
  • 18,530
  • 3
  • 50
  • 70
1

You can use the utility type Readonly<T> to mark the type on the BehaviorSubject as Readonly<User> instead of User:

let bs = new BehaviorSubject<Readonly<User>>(user);

Now, TS will warn you when you try to modify:

screenshot

Cannot assign to 'firstName' because it is a read-only property. (2540)

BizzyBob
  • 12,309
  • 4
  • 27
  • 51
  • How does it work with Property level 2? – cuongle Nov 16 '21 at 14:59
  • Good point, this will not warn when assigning nested properties. You may find [this SO question](https://stackoverflow.com/questions/41879327/deepreadonly-object-typescript) and this [github issue](https://github.com/microsoft/TypeScript/issues/13923) of interest. They mention some ways to implement a `DeepReadonly`. – BizzyBob Nov 17 '21 at 13:29
1

This is a problem I've had to solve several times. As long as you are passing simple non-circular objects through the subject this solution should work.

The one requirement is that you utilize the lodash library, which supports a very fast deep copy (no reference copies). I've looked everywhere and I've yet to find one that is faster.

let bs = (new BehaviorSubject<User>(user)).pipe(    
     map((userData) => _.cloneDeep(userData)) 
);

I've used this on fairly large state objects and I've always been impressed by how quickly it is able to perform copies. The implication here is that any emission coming out of the subject is a copy of the original, so changes happen in isolation and will never be able to trail back to the source.

Damian C
  • 2,111
  • 2
  • 15
  • 17