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

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