DEV Community

Cover image for Object.freeze() Goes Hard 🥶❄️

Object.freeze() Goes Hard 🥶❄️

Matt Lewandowski on January 19, 2025

The title says it all. Let's talk about one of JavaScript's most underrated features: Object.freeze(). This powerhouse of immutability isn't just a...
Collapse
 
abustamam profile image
Rasheed Bustamam

Great writeup! I think TypeScript makes Object.freeze a bit redundant though. You can annotate props as readonly which will cause a type error, and the as const will also work with keyof.

But without TS, Object.freeze can definitely save your butt!

Collapse
 
mattlewandowski93 profile image
Matt Lewandowski

While TypeScript's readonly is great, Object.freeze() provides runtime protection that type-level immutability can't catch. This is especially important for:

  1. State Management: In Redux/Zustand, TypeScript won't stop runtime mutations that can cause bugs
  2. Library Authors: Can't guarantee all users will use TypeScript
  3. Deep Freezing: Protects nested objects that readonly can't catch

I see them as complementary tools - TypeScript for compile-time safety and Object.freeze() for runtime guarantees

Collapse
 
ellockie profile image
Lukasz Przewlocki

Re #3, as you mentioned in the article, it doesn't deep-freeze, only shallow freezes, unless I don't understand something here.

For example:

const a = Object.freeze({b: {c: {d: 7}}});
console.log(a.b.c.d); // 7
a.b.c.d = 88;
console.log(a.b.c.d); // 88
Enter fullscreen mode Exit fullscreen mode
Collapse
 
pseudoresonance profile image
PseudoResonance

I just wanted to correct this one mistake, #3, readonly can't "freeze" nested objects. I found this nice piece of code somewhere.

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

export type Immutable<T> = {
    // eslint-disable-next-line @typescript-eslint/ban-types
    readonly [K in keyof T]: T[K] extends Function
        ? T[K]
        : ImmutableObject<T[K]>;
};

type MutableObject<T> = {
    -readonly [K in keyof T]: Mutable<T[K]>;
};

export type Mutable<T> = {
    // eslint-disable-next-line @typescript-eslint/ban-types
    -readonly [K in keyof T]: T[K] extends Function
        ? T[K]
        : MutableObject<T[K]>;
};
Enter fullscreen mode Exit fullscreen mode

Along with your previous example, the following shouldn't work.

const immutableUser: Immutable<User> = createUser('alice');
immutableUser.admin = true;
Enter fullscreen mode Exit fullscreen mode

I used it myself to protect against any accidents. I needed to keep the state immutable, so I had a method to deep copy, cast to the mutable object, make the changes, then cast it back to being immutable.

Collapse
 
bernardigiri profile image
Bernard Igiri

This is why I prefer Rust.

Collapse
 
aceix profile image
the_aceix

wasm your way out 😂

Collapse
 
daviddanielng profile image
David Daniel

Rust on the web?

Collapse
 
cohlar profile image
cohlar

Nice.
About safeFreeze() - couldn't this approach trigger issues in production (e.g. runtime mutations) which the developer wouldn't catch in their dev environment, and make it harder to identify and debug the issue once it occurs?

Collapse
 
mikko_rantalainen_03ce77b profile image
Mikko Rantalainen

Was this article supposed to be about JavaScript or TypeScript? As far as I know, the example syntax

const FROZEN_CONFIG: Config = Object...
Enter fullscreen mode Exit fullscreen mode

is only valid for TypeScript code. However, the article continuously spoke about JavaScript. Which way is it on reality?

Collapse
 
rutexd profile image
rutexd

The solution to a problem which never exists... If you mutating event or default state or something then you make something obliviously wrong....

Collapse
 
lordjnux profile image
Jeroham Sanchez

@mattlewandowski93 Thanks a lot for share this practicals uses of Object.freeze!

Collapse
 
darkgl profile image
Rafal

Unfortunately Object.freeze comes with hefty performance penalty

docs.google.com/document/d/1X6zO5F...