Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

I want totally immutable object in TS

I have some big object, like

const a={  b:33,   c:[78, 99],   d:{e:{f:{g:true, h:{boom:'selecta'}}}};/// well, even deeper than this... 

And I'd like TS not to allow me to do

a.d.e.f.h.boom='respek'; 

How can I immutate the object completely? Is it only by creating interface with "readonly" and interfaces for each deeply nested object?

like image 778
shal Avatar asked Mar 30 '17 13:03

shal


People also ask

How do you make an object immutable in TypeScript?

Immutability using the readonly keyword. The const keyword is a JavaScript keyword which means you can make a variable immutable natively. However, TypeScript provides a readonly keyword that can be used as a compile-time check to avoid mutation of object properties, class properties, array, etc.

Why would you want an immutable object?

Immutable objects are thread-safe so you will not have any synchronization issues. Immutable objects are good Map keys and Set elements, since these typically do not change once created. Immutability makes it easier to parallelize your program as there are no conflicts among objects.

Are TypeScript strings immutable?

But strings in JavaScript are different. They are immutable primitives. This means that the characters within them may not be changed and that any operations on strings actually create new strings.

Which objects should be called immutable?

The immutable objects are objects whose value can not be changed after initialization. We can not change anything once the object is created. For example, primitive objects such as int, long, float, double, all legacy classes, Wrapper class, String class, etc. In a nutshell, immutable means unmodified or unchangeable.


2 Answers

We now have the option as const which is a syntactically concise way of what @phil294 mentioned as the first option (nested readonly).

const a = {     b: 33,     c: [78, 99],     d:{e:{f:{g:true, h:{boom:'selecta'}}}} } as const;  a.d.e.f.h.boom = 'respek'; //Cannot assign to 'boom' because it is a read-only property.ts(2540) 

As an added bonus, you can make inputs to functions nested immutable using this trick:

type Immutable<T> = {     readonly [K in keyof T]: Immutable<T[K]>; } 

so this would happen

const a = {     b: 33,     c: [78, 99],     d:{e:{f:{g:true, h:{boom:'selecta'}}}} }  function mutateImmutable(input: Immutable<typeof a>) {     input.d.e.f.h.boom = 'respek'; //Cannot assign to 'boom' because it is a read-only property.ts(2540) } 
like image 130
Ozymandias Avatar answered Sep 21 '22 06:09

Ozymandias


As described in https://www.typescriptlang.org/docs/handbook/interfaces.html, you can use readonly on class/interface properties or Readonly<...>/ReadonlyArray<> for immutable objects and arrays. In your case, this would look like the following:

const a: Readonly<{     b: number,     c: ReadonlyArray<number>,     d: Readonly<{         e: Readonly<{             f: Readonly<{                 g: boolean,                 h: Readonly<{                     boom: string                 }>             }>         }>     }> }> = {         b: 33,         c: [78, 99],         d:{e:{f:{g:true, h:{boom:'selecta'}}}} }  a.d.e.f.h.boom = 'respek'; // error: Cannot assign to 'boom' because it is a constant or a read-only property. 

Obviously, this is quite the tautological statement, so I suggest you define proper class structure for your object. You are not really taking advantage of any of Typescript's features by just declaring a nested, untyped object.

But if you really need to go without type definitions, I think the only way is defining a freezer (love the term :D) like Hampus suggested. Taken from deepFreeze(obj) function from MDN:

function freezer(obj) {     Object.getOwnPropertyNames(obj).forEach(name => {         if (typeof obj[name] == 'object' && obj[name] !== null)             freezer(obj[name]);     });     return Object.freeze(obj); }  const a = freezer({     b:33,      c:[78, 99],      d:{e:{f:{g:true, h:{boom:'selecta'}}}}});  a.d.e.f.h.boom='respek'; // this does NOT throw an error. it simply does not override the value. 

tl;dr: You cannot get compiler type errors without defining types. That is the whole point of Typescript.

edit:

this very last statement is wrong. For example,

let a = 1 a = "hello" 

will throw an error because the type is implicitly set to number. For readonly however, I think, you will need proper declaration as defined above.

like image 30
phil294 Avatar answered Sep 22 '22 06:09

phil294