DEV Community

Cover image for Object.freeze() Goes Hard πŸ₯Άβ„️
Matt Lewandowski
Matt Lewandowski

Posted on

124 9 7 9 8

Object.freeze() Goes Hard πŸ₯Άβ„️

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 another method - it's your secret weapon for writing safer, more predictable code ✨.

I'll be honest, when I first discovered Object.freeze(), I mostly ignored it. "Just don't mutate your objects," I thought. But as my applications grew more complex, I started to see its true value. Now, it's an essential part of my toolkit.

7 Game-Changing Use Cases for Object.freeze() 🧊

Let me show you why Object.freeze() is absolutely essential and how it can level up your JavaScript game. Let's dive into some real-world examples! πŸ₯Ά

1: Constants That Are Actually Constant

We've all been there - creating "constant" objects that end up getting mutated somewhere in our codebase.

interface Config {
  api: string;
  timeout: number;
  retries: number;
}

// Without freeze - supposedly "constant" but can be modified
const CONFIG: Config = {
  api: "https://api.example.com",
  timeout: 5000,
  retries: 3
};

// Oops! This works even though it's "constant"
CONFIG.timeout = 1000;

// With freeze - truly immutable
const FROZEN_CONFIG: Config = Object.freeze({
  api: "https://api.example.com",
  timeout: 5000,
  retries: 3
});

// This throws an error in strict mode! πŸŽ‰
FROZEN_CONFIG.timeout = 1000;
Enter fullscreen mode Exit fullscreen mode

Now your configuration is actually immutable. No more debugging mysterious config changes.

2: Protecting Default State

This one's a game-changer for state management, especially in Redux or similar solutions.

interface AppState {
  user: {
    name: string;
    preferences: {
      theme: 'light' | 'dark';
      notifications: boolean;
    };
  };
  settings: {
    language: string;
  };
}

const initialState: AppState = Object.freeze({
  user: {
    name: '',
    preferences: {
      theme: 'light',
      notifications: true
    }
  },
  settings: {
    language: 'en'
  }
});

// Now you can't accidentally modify your initial state!
// This will throw in strict mode
initialState.user.preferences.theme = 'dark';
Enter fullscreen mode Exit fullscreen mode

3: Enum-like Objects That Can't Be Modified

JavaScript doesn't have true enums, but with Object.freeze() we can get pretty close:

const HttpStatus = Object.freeze({
  OK: 200,
  CREATED: 201,
  BAD_REQUEST: 400,
  UNAUTHORIZED: 401,
  NOT_FOUND: 404,
  INTERNAL_SERVER_ERROR: 500,
  // TypeScript bonus: as const for literal types
} as const);

// This will throw! Your status codes are safe πŸ”’
HttpStatus.OK = 999;

// TypeScript knows the exact types
type StatusCode = typeof HttpStatus[keyof typeof HttpStatus];
// Type is exactly: 200 | 201 | 400 | 401 | 404 | 500
Enter fullscreen mode Exit fullscreen mode

4: Deep Freezing Objects

Object.freeze() is shallow by default, but we can create a deep freeze utility:

function deepFreeze<T>(obj: T): Readonly<T> {
  // Get all properties, including non-enumerable ones
  const propNames = Object.getOwnPropertyNames(obj);

  // Freeze properties before freezing parent
  propNames.forEach(name => {
    const value = (obj as any)[name];

    if (value && typeof value === 'object') {
      deepFreeze(value);
    }
  });

  return Object.freeze(obj);
}

const complexObject = deepFreeze({
  level1: {
    level2: {
      level3: {
        value: "can't touch this"
      }
    }
  }
});

// This will throw! Deep freezing for the win πŸ†
complexObject.level1.level2.level3.value = "try to touch this";
Enter fullscreen mode Exit fullscreen mode

5: Protecting Event Handlers

Ever had bugs because an event object was modified after the event? Not anymore!

interface CustomEvent {
  type: string;
  timestamp: number;
  data: any;
}

class EventEmitter {
  private handlers: { [key: string]: Function[] } = {};

  emit(event: CustomEvent) {
    // Freeze the event object to prevent modifications
    const frozenEvent = Object.freeze({ ...event });

    const handlers = this.handlers[event.type] || [];
    handlers.forEach(handler => handler(frozenEvent));
  }

  on(type: string, handler: Function) {
    if (!this.handlers[type]) {
      this.handlers[type] = [];
    }
    this.handlers[type].push(handler);
  }
}
Enter fullscreen mode Exit fullscreen mode

6: Immutable API Responses

Keep your API responses immutable to prevent accidental modifications:

interface ApiResponse<T> {
  data: T;
  metadata: {
    timestamp: number;
    requestId: string;
  };
}

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  const json = await response.json();

  // Freeze the response immediately
  return Object.freeze({
    data: json,
    metadata: {
      timestamp: Date.now(),
      requestId: crypto.randomUUID()
    }
  });
}

// Usage
const response = await fetchData('https://api.example.com/data');
// This will throw! Your API response is safe πŸ›‘οΈ
response.data = null;
Enter fullscreen mode Exit fullscreen mode

7: Creating Truly Private Properties

While we now have private fields with #, Object.freeze() can help create truly private properties in factory functions:

interface User {
  readonly id: string;
  readonly username: string;
  getInfo(): string;
}

function createUser(username: string): User {
  const privateState = {
    loginAttempts: 0,
    lastLogin: new Date()
  };

  // Public interface is frozen
  return Object.freeze({
    id: crypto.randomUUID(),
    username,
    getInfo() {
      return `${username} (Last login: ${privateState.lastLogin})`;
    }
  });
}

const user = createUser('alice');
// This throws! Can't add properties
user.admin = true;
// This throws! Can't modify existing ones
user.username = 'bob';
Enter fullscreen mode Exit fullscreen mode

Performance Considerations πŸš€

While Object.freeze() is powerful, it's important to understand its performance implications:

  1. The freezing operation itself has a cost, especially with deepFreeze
  2. Frozen objects can be slightly slower to read from (but the difference is usually negligible)
  3. TypeScript's readonly is compile-time only and has zero runtime cost

Here's a performance-conscious approach:

// Development: Use Object.freeze() for better error catching
const isDev = process.env.NODE_ENV === 'development';

function safeFreeze<T>(obj: T): Readonly<T> {
  return isDev ? Object.freeze(obj) : obj;
}

// Now use safeFreeze everywhere
const config = safeFreeze({
  // your config here
});
Enter fullscreen mode Exit fullscreen mode

TypeScript Magic ✨

Object.freeze() works beautifully with TypeScript's type system:

// Object.freeze() automatically makes properties readonly
const frozen = Object.freeze({ x: 1, y: 2 });
// TypeScript error: Cannot assign to 'x' because it is a readonly property
frozen.x = 2;

// Works with 'as const' for literal types
const DIRECTIONS = Object.freeze({
  UP: 'UP',
  DOWN: 'DOWN',
  LEFT: 'LEFT',
  RIGHT: 'RIGHT'
} as const);

// Type is exactly: 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'
type Direction = typeof DIRECTIONS[keyof typeof DIRECTIONS];
Enter fullscreen mode Exit fullscreen mode

Conclusion

Object.freeze() might seem simple at first glance, but it's an incredibly powerful tool for writing safer, more maintainable JavaScript. From protecting configuration objects to ensuring immutable state, it's an essential part of modern JavaScript development.

The next time you find yourself reaching for a third-party immutability library, remember that Object.freeze() might be all you need! πŸ₯Άβœ¨

Oh and one last shameless plug 😁

Kollabe retrospective

If you're looking to run retrospectives or planning poker sessions, check out Kollabe. It's a robust, production-tested platform that's constantly evolving to create the best real-time collaboration experience possible.

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (12)

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
 
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
 
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
 
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...

The Most Contextual AI Development Assistant

Pieces.app image

Our centralized storage agent works on-device, unifying various developer tools to proactively capture and enrich useful materials, streamline collaboration, and solve complex problems through a contextual understanding of your unique workflow.

πŸ‘₯ Ideal for solo developers, teams, and cross-company projects

Learn more

πŸ‘‹ Kindness is contagious

Engage with a sea of insights in this enlightening article, highly esteemed within the encouraging DEV Community. Programmers of every skill level are invited to participate and enrich our shared knowledge.

A simple "thank you" can uplift someone's spirits. Express your appreciation in the comments section!

On DEV, sharing knowledge smooths our journey and strengthens our community bonds. Found this useful? A brief thank you to the author can mean a lot.

Okay