21

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?

shal
  • 2,110
  • 3
  • 15
  • 25
  • I would suggest you use a library for that something like immutable.js – toskv Mar 30 '17 at 14:10
  • Please see my answer at https://stackoverflow.com/questions/41879327/deepreadonly-object-typescript/49670389 for a DeepReadonly type that is possible as of TS 2.8. – zenmumbler Apr 09 '18 at 08:37

5 Answers5

14

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)
}
AjaxLeung
  • 1,588
  • 17
  • 24
7

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.

phil294
  • 8,501
  • 7
  • 55
  • 84
1

Minko Gechev has created DeepReadonly types:

type DeepReadonly<T> =
  T extends (infer R)[] ? DeepReadonlyArray<R> :
  T extends Function ? T :
  T extends object ? DeepReadonlyObject<T> :
  T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};

interface Person {
  name: string;
  job: { company: string, position:string };
}

const person: DeepReadonly<Person> = {
  name: 'Minko',
  job: {
    company: 'Google',
    position: 'Software engineer'
  }
};

person.job.company = 'Alphabet'; // Error
Martin Jaskulla
  • 343
  • 2
  • 8
0

Have a look at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze or https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

Object.freeze might do what you want but at least WebStorm does not warn you when trying to edit the object and Chrome fails silently.

const obj = { val: 1 }; 
Object.freeze(obj);
obj.val = 2;

console.log(obj);
-> { val: 1 }

Nothing can be added to or removed from the properties set of a frozen object. Any attempt to do so will fail, either silently or by throwing a TypeError exception

Hampus
  • 2,609
  • 1
  • 15
  • 36
  • Doesn't work for nested objects: `let a = Object.freeze({ b: { c: 'c' } }); a.b.c = 'd'; a.b` – Pavlo Mar 30 '17 at 14:40
  • Not only doesn't work for nested objects, but also not a part of TS, I need TS compiler errors. – shal Mar 30 '17 at 14:43
  • 1
    Well, one can build a freezer that also freezes the nested objects :) – Hampus Mar 30 '17 at 14:43
  • @shal I don't know of any immutability support from typescript on variables. You might be able to add checkstyle rules for warning when writing to frozen objects. – Hampus Mar 30 '17 at 14:47
  • I know that I can do it by defining interfaces with `readonly` for each property and by making interface for each nested object, but it's a nightmare, too much work... – shal Mar 30 '17 at 14:50
  • I dont know if there is a way to use a decorator to do the manual labour part of it – Hampus Mar 30 '17 at 14:51
0

This works:

const a= new class {
    readonly b = 33, 
    readonly c:ReadonlyArray<number> = [78, 99], 
    readonly d = new class {
        readonly e = new class {
            readonly f = new class {
                readonly g:true, 
                readonly h: new class {
                    readonly boom:'selecta'}}}};
user3048859
  • 192
  • 6