,

The Spread Operator with Objects: Best Practices

Posted by

From cloning to merging configs, learn how to use object spread ({...obj}) in JavaScript and TypeScript without introducing subtle bugs.

From cloning to merging configs, learn how to use object spread ({...obj}) in JavaScript and TypeScript without introducing subtle bugs.

Introduction

You’ve seen the spread operator (...) with arrays. But with ES2018, object spread became a first-class feature:

const clone = { ...original };

It looks simple, but beneath the surface, object spread has important rules and quirks that can affect performance, mutability, and correctness.

In this post, we’ll cover the best practices for using object spread, how it compares to alternatives, and real-world use cases (React props, API responses, configs). By the end, you’ll know not just how to use it, but how to use it safely.


1. Quick Refresher: How Object Spread Works

const base = { a: 1, b: 2 };
const copy = { ...base };

console.log(copy); // { a: 1, b: 2 }
  • Copies own enumerable properties from the source.
  • Works left → right; later properties overwrite earlier ones.
  • Shallow copy (only one level deep).

👉 Think of it as “clone and override” for objects.


2. Cloning Objects

const user = { id: 1, name: "Ali" };
const clone = { ...user };

console.log(clone === user); // false (different reference)
✅ Use for immutable updates.
⚠️ Remember: shallow copy. Nested objects are still shared:
const nested = { a: { b: 1 } };
const clone = { ...nested };

clone.a.b = 99;
console.log(nested.a.b); // 99 😬

Best Practice: For deep copies, use structuredClone(), JSON.parse(JSON.stringify()), or a library.


3. Merging Objects

const defaults = { retries: 3, timeout: 1000 };
const overrides = { timeout: 5000 };

const config = { ...defaults, ...overrides };
console.log(config);

// { retries: 3, timeout: 5000 }
  • Later spreads override earlier ones.
  • Order matters!

Best Practice: Put defaults first, overrides last.


4. Adding / Overriding Properties Inline

const user = { id: 1, name: "Umar" };
const withRole = { ...user, role: "admin" };

console.log(withRole);
// { id: 1, name: "Umar", role: "admin" }

👉 Cleaner than Object.assign or mutation.


5. Removing Properties (The “omit” Trick)

const user = { id: 1, name: "Umar", password: "secret" };
const { password, ...publicData } = user;

console.log(publicData);
// { id: 1, name: "Umar" }

👉 Great for stripping sensitive fields before sending to APIs.


6. Combining with Destructuring

const { email, ...rest } = {
id: 1,
email: "e@example.com",
role: "admin"
};

console.log(rest);
// { id: 1, role: "admin" }

Best Practice: Use this for “picking and omitting” fields in a clean way.


7. Spread with Dynamic Properties

const dynamicKey = "status";
const obj = { ...{ [dynamicKey]: "active" }, role: "user" };

console.log(obj);
// { status: "active", role: "user" }

👉 Works well with computed keys and API-driven values.


8. Object Spread in React Props

Passing props down

function Button(props) {
return <button {...props} />;
}

<Button type="submit" className="btn" disabled />;

Merging defaults with overrides

const baseProps = { type: "button", className: "btn" };
const extraProps = { disabled: true };

<Button {...baseProps} {...extraProps} />;
// Later props override earlier ones

⚠️ Best Practice:

  • Spread props last if you want them to override defaults.
  • Spread props first if you want to enforce certain defaults.

9. Gotchas and Quirks

Shallow copy only

Nested objects aren’t cloned.

Non-enumerable & symbol properties

They aren’t copied.

const sym = Symbol("x");
const obj = { a: 1, [sym]: 2 };

const spread = { ...obj };
console.log(spread); // { a: 1 } (no symbol)

Inheritance is ignored

Spread copies only own properties, not inherited ones.

const base = Object.create({ inherited: true });
base.a = 1;
const clone = { ...base };

console.log(clone); // { a: 1 } (no inherited prop)

10. Performance Considerations

  • Spread is generally fast for small/medium objects.
  • For large objects or frequent deep copies, prefer specialized libraries.
  • Avoid unnecessary spreading in hot loops — it creates new objects every iteration.

11. TypeScript Best Practices

A) With Partial Types

type Config = { retries: number; timeout: number };
const defaultConfig: Config = { retries: 3, timeout: 1000 };

const config: Config = { ...defaultConfig, timeout: 5000 };

B) With Pick and Omit

type User = { id: number; name: string; password: string };
type PublicUser = Omit<User, "password">;

function sanitize(user: User): PublicUser {
const { password, ...rest } = user;
return rest;
}

👉 Using Omit ensures TypeScript enforces your intent.


12. Alternatives to Object Spread

  • Object.assign({}, a, b) → same as { ...a, ...b }, but older style.
  • Libraries (lodash.merge, deepmerge) → for deep merging.
  • structuredClone(obj) → for deep clones without hacks.

👉 Rule of thumb: Use spread for shallow, predictable merges; use specialized tools when you need deep merging.


Conclusion

The object spread operator ({...obj}) is one of JavaScript’s cleanest modern features. But like most tools, it’s easy to misuse if you don’t know its limits.

  • ✅ Use for shallow clones, merging configs, stripping fields, and React props.
  • ⚠️ Beware of shallow copy pitfalls, symbol omissions, and performance overhead.
  • 💡 Pair with TypeScript’s utility types (Omit, Partial) for maximum safety.

Pro Tip: Treat object spread as a surgical tool, not a hammer. Use it where it improves readability and safety; avoid using it blindly for deep structures.


Call to Action (CTA)

What’s your favorite object spread trick (or worst bug it caused)?
Drop your story in the comments, and share this article with a teammate who still writes Object.assign everywhere.

Leave a Reply

Your email address will not be published. Required fields are marked *