Last active
November 4, 2024 14:55
-
-
Save tkrotoff/e997cd6ff8d6cf6e51e6bb6146407fc3 to your computer and use it in GitHub Desktop.
Deeply freezes an object
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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