Reputation: 3024
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?
Upvotes: 46
Views: 30633
Reputation: 2759
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 ImmutableObject<T> = {
readonly [K in keyof T]: Immutable<T[K]>;
}
export type Immutable<T> = {
readonly [K in keyof T]: T[K] extends Function ? T[K] : ImmutableObject<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)
}
Upvotes: 47
Reputation: 126
Other OOP languages have class immutability by default, and TypeScript is no different. You can go with a classic OO way, like in Java or PHP:
export class ClassicImmutableObject {
private readonly _stringProperty: string;
private readonly _numberProperty: number;
constructor(stringProperty: string, numberProperty: number) {
this._stringProperty = stringProperty;
this._numberProperty = numberProperty;
}
get stringProperty(): string {
return this._stringProperty;
}
get numberProperty(): number {
return this._numberProperty;
}
withStringProperty(stringProperty: string): ClassicImmutableObject {
return new ClassicImmutableObject(stringProperty, this._numberProperty);
}
withNumberProperty(numberProperty: number): ClassicImmutableObject {
return new ClassicImmutableObject(this._stringProperty, numberProperty);
}
}
You can then use it in your code:
import { ClassicImmutableObject } from './classic-immutable-object';
//...
immutableObjectExperiment(): string {
const immutableObject: ClassicImmutableObject =
new ClassicImmutableObject('test string', 123);
// this is not allowed: (TS2540 Can not assign to read-only property)
//immutableObject.stringProperty = 'modified';
let result = `Original classic immutable object: ${JSON.stringify(immutableObject)} \r\n`;
result += 'This demonstrates how to use getters:\r\n';
result += `stringProperty value: ${immutableObject.stringProperty}\r\n`;
result += `numberProperty value: ${immutableObject.numberProperty}\r\n\r\n`;
const modifiedImmutableObject: ClassicImmutableObject =
immutableObject.withStringProperty('modified test string');
result += `Modified classic immutable object with only stringProperty changed: ${JSON.stringify(modifiedImmutableObject)}\r\n`;
result += `Original immutable object is still available and unchanged: ${JSON.stringify(immutableObject)}`;
return result;
}
The result of this function should be:
Original classic immutable object: {"_stringProperty":"test string","_numberProperty":123}
This demonstrates how to use getters:
stringProperty value: test string
numberProperty value: 123
Modified classic immutable object with only stringProperty changed: {"_stringProperty":"modified test string","_numberProperty":123}
Original immutable object is still available and unchanged: {"_stringProperty":"test string","_numberProperty":123}
Upvotes: 1
Reputation: 482
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
Upvotes: 6
Reputation: 192
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'}}}};
Upvotes: 0
Reputation: 10822
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.
Upvotes: 9
Reputation: 2799
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
Upvotes: 0