Skip to content

Instantly share code, notes, and snippets.

@tkrotoff
Last active April 18, 2024 08:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save tkrotoff/e997cd6ff8d6cf6e51e6bb6146407fc3 to your computer and use it in GitHub Desktop.
Save tkrotoff/e997cd6ff8d6cf6e51e6bb6146407fc3 to your computer and use it in GitHub Desktop.
Deeply freezes an object
import { ReadonlyDeep } from 'type-fest';
/**
* Deeply freezes an object by recursively freezing all of its properties.
*
* - https://gist.github.com/tkrotoff/e997cd6ff8d6cf6e51e6bb6146407fc3
* - https://stackoverflow.com/a/69656011
*
* FIXME Should be part of Lodash: https://github.com/Maggi64/moderndash/issues/139
*
* Does not work with Set and Map: https://stackoverflow.com/q/31509175
*/
export function deepFreeze<
T
// Can cause: "Type instantiation is excessively deep and possibly infinite."
//extends Jsonifiable
>(obj: T) {
// @ts-expect-error
Object.values(obj).forEach(value => Object.isFrozen(value) || deepFreeze(value));
return Object.freeze(obj) as ReadonlyDeep<T>;
}
import { deepFreeze } from './deepFreeze';
test('object', () => {
{
let obj = { foo: { bar: 'baz' } };
expect(Object.isFrozen(obj)).toBe(false);
expect(Object.isFrozen(obj.foo)).toBe(false);
expect(Object.isFrozen(obj.foo.bar)).toBe(true); // WTF
obj.foo.bar = 'corge';
expect(obj).toEqual({ foo: { bar: 'corge' } });
// @ts-expect-error
obj.foo = { qux: 'quux' };
expect(obj).toEqual({ foo: { qux: 'quux' } });
// @ts-expect-error
obj = { qux: 'quux' };
expect(obj).toEqual({ qux: 'quux' });
}
{
let obj = { foo: { bar: 'baz' } };
Object.freeze(obj);
expect(Object.isFrozen(obj)).toBe(true);
expect(Object.isFrozen(obj.foo)).toBe(false);
expect(Object.isFrozen(obj.foo.bar)).toBe(true); // WTF
expect(
() =>
(obj.foo = {
// @ts-expect-error
qux: 'quux'
})
).toThrow("Cannot assign to read only property 'foo' of object '#<Object>'");
expect(obj).toEqual({ foo: { bar: 'baz' } });
obj.foo.bar = 'corge';
expect(obj).toEqual({ foo: { bar: 'corge' } });
// @ts-expect-error
obj = { qux: 'quux' };
expect(obj).toEqual({ qux: 'quux' });
}
{
let obj = { foo: { bar: 'baz' } };
deepFreeze(obj);
expect(obj).toEqual({ foo: { bar: 'baz' } });
expect(Object.isFrozen(obj)).toBe(true);
expect(Object.isFrozen(obj.foo)).toBe(true);
expect(Object.isFrozen(obj.foo.bar)).toBe(true);
expect(
() =>
(obj.foo = {
// @ts-expect-error
qux: 'quux'
})
).toThrow("Cannot assign to read only property 'foo' of object '#<Object>'");
expect(obj).toEqual({ foo: { bar: 'baz' } });
expect(() => (obj.foo.bar = 'corge')).toThrow(
"Cannot assign to read only property 'bar' of object '#<Object>'"
);
expect(obj).toEqual({ foo: { bar: 'baz' } });
// @ts-expect-error
obj = { qux: 'quux' };
expect(obj).toEqual({ qux: 'quux' });
}
});
test('array', () => {
const arr = [1, 2, 3];
expect(Object.isFrozen(arr)).toBe(false);
deepFreeze(arr);
expect(Object.isFrozen(arr)).toBe(true);
expect(() => (arr[0] = 0)).toThrow(
"Cannot assign to read only property '0' of object '[object Array]'"
);
expect(arr).toEqual([1, 2, 3]);
});
test("string - doesn't make sense", () => {
const str = 'foo';
expect(Object.isFrozen(str)).toBe(true);
deepFreeze(str);
expect(Object.isFrozen(str)).toBe(true);
expect(str).toBe('foo');
});
test("number - doesn't make sense", () => {
const num = 11;
expect(Object.isFrozen(num)).toBe(true);
deepFreeze(num);
expect(Object.isFrozen(num)).toBe(true);
expect(num).toBe(11);
});
test("boolean - doesn't make sense", () => {
const bool = true;
expect(Object.isFrozen(bool)).toBe(true);
deepFreeze(bool);
expect(Object.isFrozen(bool)).toBe(true);
expect(bool).toBe(true);
});
test("function - doesn't make sense", () => {
// eslint-disable-next-line unicorn/consistent-function-scoping
function value() {
return 'foobar';
}
expect(Object.isFrozen(value)).toBe(false);
deepFreeze(value);
expect(Object.isFrozen(value)).toBe(true);
expect(value()).toBe('foobar');
});
test("null - doesn't make sense", () => {
// eslint-disable-next-line unicorn/no-null
const value = null;
expect(Object.isFrozen(value)).toBe(true);
expect(() => deepFreeze(value)).toThrow('Cannot convert undefined or null to object');
expect(Object.isFrozen(value)).toBe(true);
expect(value).toBeNull();
});
test("undefined - doesn't make sense", () => {
const value = undefined;
expect(Object.isFrozen(value)).toBe(true);
expect(() => deepFreeze(value)).toThrow('Cannot convert undefined or null to object');
expect(Object.isFrozen(value)).toBe(true);
expect(value).toBeUndefined();
});
test('Set - not working', () => {
const set = new Set([1, 2, 3]);
expect(Object.isFrozen(set)).toBe(false);
deepFreeze(set);
expect(Object.isFrozen(set)).toBe(true);
set.add(4);
expect(set).toEqual(new Set([4, 1, 2, 3]));
});
test('Map - not working', () => {
const map = new Map([
['a', 1],
['b', 2],
['c', 3]
]);
expect(Object.isFrozen(map)).toBe(false);
deepFreeze(map);
map.set('d', 4);
expect(Object.isFrozen(map)).toBe(true);
expect(map).toEqual(
new Map([
['a', 1],
['b', 2],
['c', 3],
['d', 4]
])
);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment