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;
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';
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
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";
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);
}
}
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;
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';
Performance Considerations ๐
While Object.freeze() is powerful, it's important to understand its performance implications:
- The freezing operation itself has a cost, especially with deepFreeze
- Frozen objects can be slightly slower to read from (but the difference is usually negligible)
- 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
});
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];
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 ๐
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.
Top comments (1)
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!