「Object.keysとObject.entriesってどう違うの?」「分割代入を使いこなしたい」「Immutableな更新ってどうやるの?」――JavaScriptで開発をしていると、オブジェクト操作の壁に何度もぶつかります。配列に比べてObjectのAPIは地味ですが、業務コードの大半はオブジェクトの操作で構成されていると言っても過言ではありません。
本記事では、ES2025までに追加されたObject関連APIを総ざらいし、30個以上のコピペで動くサンプルコードとともに、現役エンジニアが本当に使う実践パターンを徹底解説します。Object.keys/values/entriesから、Object.groupBy、structuredClone、Immerによる不変更新まで、明日から使える内容に絞ってお届けします。
- Object.keys / values / entries / fromEntriesの使い分け
- 浅いコピーと深いコピー(structuredClone)の正しい選択
- 分割代入・デフォルト値・リネーム・ネスト展開の全パターン
- ImmutableUpdate(spread / Immer / lodash)による安全な状態管理
- Object.groupBy(ES2024)による集計の現代的な書き方
- Map vs Object の判断基準とパフォーマンス比較
- 1. Object.keys / values / entries — 3兄弟の使い分け
- 2. Object.fromEntries — 配列やMapからオブジェクトへ
- 3. オブジェクトのコピー — 浅い・深いの違いを完全理解
- 4. オブジェクトを固める — freeze / seal / preventExtensions
- 5. プロトタイプ操作 — Object.create / getPrototypeOf
- 6. プロパティディスクリプタの世界
- 7. プロパティ存在チェックの正解 — Object.hasOwn
- 8. 分割代入を極める
- 9. オプショナルチェイン と Nullish coalescing
- 10. Object.groupBy — ES2024の新機能
- 11. JSON との行き来
- 12. Map vs Object — どちらを使うべきか
- 13. Immutable更新パターン — Reactの必須テクニック
- 14. Immer — Immutable更新を直感的に書く
- 15. lodash の get / set — 動的なパス指定
- 16. TypeScriptでのオブジェクト型推論の限界
- 17. パフォーマンス考察 — 何が速くて何が遅いか
- 18. 現役エンジニアが本気で学ぶなら
- 19. まとめ — 明日から使えるObject操作
1. Object.keys / values / entries — 3兄弟の使い分け
オブジェクト操作の基本にして最重要のAPIが、Object.keys / Object.values / Object.entriesの3兄弟です。配列メソッド(map / filter / reduce)と組み合わせることで、ほぼすべてのオブジェクト変換が表現できます。
1-1. Object.keys でキー一覧を取得
const user = { id: 1, name: "Taro", age: 28, email: "taro@example.com" };
const keys = Object.keys(user);
console.log(keys);
// ["id", "name", "age", "email"]
// プロパティ数を数える
console.log(Object.keys(user).length); // 4
// for...inと異なりプロトタイプチェーンは辿らない
const proto = { inherited: "ignored" };
const child = Object.create(proto);
child.own = "visible";
console.log(Object.keys(child)); // ["own"] のみ
1-2. Object.values で値一覧を取得
const scores = { math: 80, english: 92, science: 75 };
const values = Object.values(scores);
console.log(values); // [80, 92, 75]
// 合計と平均
const total = Object.values(scores).reduce((a, b) => a + b, 0);
const avg = total / Object.values(scores).length;
console.log(total, avg); // 247 82.33...
// 最高点
console.log(Math.max(...Object.values(scores))); // 92
1-3. Object.entries でキー・値のペアを取得
const config = { host: "localhost", port: 5432, ssl: true };
for (const [key, value] of Object.entries(config)) {
console.log(`${key} = ${value}`);
}
// host = localhost
// port = 5432
// ssl = true
// クエリ文字列に変換
const params = { page: 1, limit: 20, sort: "asc" };
const qs = Object.entries(params)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join("&");
console.log(qs); // page=1&limit=20&sort=asc
1-4. オブジェクトの map / filter を自前で実装
配列にはmap / filterがありますが、オブジェクトには存在しません。Object.entries + Object.fromEntriesの組み合わせで実装できます。
// オブジェクトのvalueをすべて2倍にする
const prices = { apple: 100, banana: 50, cherry: 300 };
const doubled = Object.fromEntries(
Object.entries(prices).map(([k, v]) => [k, v * 2])
);
console.log(doubled);
// { apple: 200, banana: 100, cherry: 600 }
// 200円以上だけ残す
const expensive = Object.fromEntries(
Object.entries(prices).filter(([_, v]) => v >= 200)
);
console.log(expensive); // { cherry: 300 }
1-5. ユーティリティ関数として切り出す
// TypeScriptで型安全に
function mapValues<T, U>(
obj: Record<string, T>,
fn: (value: T, key: string) => U
): Record<string, U> {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, fn(v, k)])
);
}
const result = mapValues({ a: 1, b: 2, c: 3 }, (v) => v * 10);
console.log(result); // { a: 10, b: 20, c: 30 }
2. Object.fromEntries — 配列やMapからオブジェクトへ
Object.fromEntriesはES2019で追加された、Object.entriesの逆変換を行うメソッドです。配列やMapからオブジェクトを生成する際の決定版です。
2-1. 二次元配列からオブジェクト生成
const pairs = [
["name", "Hanako"],
["age", 30],
["role", "engineer"],
];
const obj = Object.fromEntries(pairs);
console.log(obj);
// { name: "Hanako", age: 30, role: "engineer" }
2-2. URLSearchParams のパース
const url = new URL("https://example.com/?page=2&limit=10&sort=desc");
const queryObject = Object.fromEntries(url.searchParams);
console.log(queryObject);
// { page: "2", limit: "10", sort: "desc" }
2-3. FormData をオブジェクトに変換
// HTMLフォーム送信時の典型パターン
const form = document.querySelector("form");
form.addEventListener("submit", (e) => {
e.preventDefault();
const formData = new FormData(form);
const data = Object.fromEntries(formData);
console.log(data); // { username: "...", email: "...", ... }
fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
});
2-4. Map → Object 変換
const userMap = new Map([
["id", 1],
["name", "Yamada"],
]);
const userObj = Object.fromEntries(userMap);
console.log(userObj); // { id: 1, name: "Yamada" }
3. オブジェクトのコピー — 浅い・深いの違いを完全理解
「コピーしたつもりが元のオブジェクトまで書き換わっていた」というバグは、JavaScript初学者が必ず通る道です。ここを正確に理解しないと、Reactのstate更新でハマります。
3-1. Object.assign による浅いコピー
const original = { name: "Sato", age: 25, address: { city: "Tokyo" } };
const copy = Object.assign({}, original);
copy.name = "Suzuki";
console.log(original.name); // "Sato" ← 影響なし
// しかし、ネストされたオブジェクトは参照共有
copy.address.city = "Osaka";
console.log(original.address.city); // "Osaka" ← 巻き込まれる!
3-2. spread構文による浅いコピー(モダンな書き方)
const original = { a: 1, b: 2, c: { nested: true } };
const copy = { ...original };
copy.a = 999;
console.log(original.a); // 1 ← OK
// ネストは依然として共有
copy.c.nested = false;
console.log(original.c.nested); // false ← 同じ問題
3-3. structuredClone による深いコピー(ES2022)
structuredCloneはWeb標準APIで、ライブラリ不要で深いコピーが可能です。2026年現在、深いコピーの第一選択肢です。
const original = {
name: "Tanaka",
address: { city: "Tokyo", zip: "100-0001" },
tags: ["admin", "developer"],
};
const deep = structuredClone(original);
deep.address.city = "Osaka";
deep.tags.push("new");
console.log(original.address.city); // "Tokyo" ← 影響なし
console.log(original.tags); // ["admin", "developer"] ← 影響なし
3-4. structuredClone がコピーできる型
// オブジェクト・配列はもちろん、以下も対応
const complex = {
date: new Date(),
regex: /foo/g,
map: new Map([["key", "value"]]),
set: new Set([1, 2, 3]),
arrayBuffer: new ArrayBuffer(8),
};
const cloned = structuredClone(complex);
console.log(cloned.date instanceof Date); // true
console.log(cloned.map.get("key")); // "value"
// ただし関数やDOMノードはコピー不可
try {
structuredClone({ fn: () => 1 });
} catch (e) {
console.error(e.message); // "could not be cloned"
}
3-5. JSON.parse(JSON.stringify()) の罠
// 古典的な深いコピー手法だが、欠点が多い
const original = {
date: new Date("2026-01-01"),
fn: () => "hello",
undef: undefined,
num: NaN,
inf: Infinity,
};
const bad = JSON.parse(JSON.stringify(original));
console.log(bad.date); // 文字列に化ける!
console.log(bad.fn); // undefined ← 消える
console.log(bad.undef); // キーごと消失
console.log(bad.num); // null ← NaNはnullに
console.log(bad.inf); // null ← Infinityもnullに
// → structuredClone か Immer を使うべき
4. オブジェクトを固める — freeze / seal / preventExtensions
オブジェクトの変更を制限する3段階の方法があります。意図せぬ書き換えを防ぐdefensive programmingの基本です。
4-1. Object.freeze で完全に凍結
const config = Object.freeze({
API_URL: "https://api.example.com",
TIMEOUT: 5000,
});
config.TIMEOUT = 10000; // strict modeでなければ silent fail
console.log(config.TIMEOUT); // 5000
"use strict";
config.NEW = "added"; // TypeError: Cannot add property
4-2. Object.isFrozen で確認
const frozen = Object.freeze({ a: 1 });
const normal = { a: 1 };
console.log(Object.isFrozen(frozen)); // true
console.log(Object.isFrozen(normal)); // false
4-3. freeze は浅い凍結
const nested = Object.freeze({
level1: { level2: { value: 1 } },
});
// トップレベルは凍結されるが…
nested.level1 = "changed"; // 無視される
// ネストは生きている
nested.level1.level2.value = 999;
console.log(nested.level1.level2.value); // 999 ← 変わる
4-4. 深い凍結(deepFreeze)の実装
function deepFreeze(obj) {
Object.keys(obj).forEach((key) => {
const value = obj[key];
if (value && typeof value === "object") {
deepFreeze(value);
}
});
return Object.freeze(obj);
}
const truly = deepFreeze({
a: { b: { c: 1 } },
});
truly.a.b.c = 999; // strict modeでTypeError
console.log(truly.a.b.c); // 1
4-5. Object.seal — プロパティ追加・削除は禁止、値変更はOK
const sealed = Object.seal({ name: "Aoki", age: 40 });
sealed.age = 41; // OK
sealed.email = "x"; // 追加は無視
delete sealed.name; // 削除も無視
console.log(sealed); // { name: "Aoki", age: 41 }
console.log(Object.isSealed(sealed)); // true
4-6. Object.preventExtensions — 追加だけ禁止
const obj = Object.preventExtensions({ x: 1 });
obj.x = 2; // OK
delete obj.x; // OK
obj.y = 3; // 無視
console.log(Object.isExtensible(obj)); // false
console.log(obj); // {}
5. プロトタイプ操作 — Object.create / getPrototypeOf
「JavaScriptはプロトタイプベースの言語」と言われますが、実務でObject.createを意識して使う場面は限定的です。それでもライブラリ実装などで重要な知識です。
5-1. Object.create で継承
const animal = {
greet() {
return `I'm ${this.name}`;
},
};
const dog = Object.create(animal);
dog.name = "Pochi";
console.log(dog.greet()); // "I'm Pochi"
console.log(Object.getPrototypeOf(dog) === animal); // true
5-2. Object.create(null) でプレーンなオブジェクト
// 通常のオブジェクトはObject.prototypeを継承するため、
// toStringやhasOwnPropertyなどのキーを持つ
const normal = {};
console.log(normal.toString); // [Function: toString]
// 完全にクリーンなオブジェクトが欲しい場合
const clean = Object.create(null);
console.log(clean.toString); // undefined
// ハッシュマップとして使う場合に有用
clean["toString"] = "user-defined";
console.log(clean.toString); // "user-defined"
5-3. Object.setPrototypeOf(非推奨パターン)
// パフォーマンスが大幅に悪化するため避けるべき
const a = { foo: 1 };
const b = { bar: 2 };
Object.setPrototypeOf(a, b);
console.log(a.bar); // 2 ← bから継承
// ↑ 実務では Object.create か class を使うべき
6. プロパティディスクリプタの世界
普段書くobj.x = 1は、内部的にはディスクリプタを設定しています。これを直接操作することで、読み取り専用プロパティやgetter/setterを定義できます。
6-1. Object.defineProperty で読み取り専用
const user = { name: "Inoue" };
Object.defineProperty(user, "id", {
value: 100,
writable: false, // 書き換え不可
enumerable: false, // for...in / Object.keysで出ない
configurable: false // 削除・再定義不可
});
user.id = 999; // 無視
console.log(user.id); // 100
console.log(Object.keys(user)); // ["name"] のみ
6-2. getter / setter を定義
const account = {
_balance: 1000,
};
Object.defineProperty(account, "balance", {
get() {
return `¥${this._balance.toLocaleString()}`;
},
set(value) {
if (typeof value !== "number") {
throw new Error("balance must be number");
}
this._balance = value;
},
enumerable: true,
});
console.log(account.balance); // "¥1,000"
account.balance = 5000;
console.log(account.balance); // "¥5,000"
6-3. Object.defineProperties で複数同時定義
const product = {};
Object.defineProperties(product, {
name: { value: "MacBook", writable: true, enumerable: true },
price: { value: 250000, writable: false, enumerable: true },
category: { value: "PC", enumerable: false },
});
console.log(product); // { name: "MacBook", price: 250000 }
console.log(Object.keys(product)); // ["name", "price"]
6-4. Object.getOwnPropertyDescriptors で全ディスクリプタ取得
const obj = { a: 1 };
Object.defineProperty(obj, "b", {
value: 2,
writable: false,
});
const descriptors = Object.getOwnPropertyDescriptors(obj);
console.log(descriptors);
/*
{
a: { value: 1, writable: true, enumerable: true, configurable: true },
b: { value: 2, writable: false, enumerable: false, configurable: false }
}
*/
// 完全コピー(ディスクリプタごと)
const clone = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
7. プロパティ存在チェックの正解 — Object.hasOwn
ES2022で追加されたObject.hasOwnは、長年のhasOwnProperty地獄を解決した決定版です。
7-1. Object.hasOwn(ES2022)
const obj = { name: "Hashimoto", age: undefined };
console.log(Object.hasOwn(obj, "name")); // true
console.log(Object.hasOwn(obj, "age")); // true(値がundefinedでも true)
console.log(Object.hasOwn(obj, "foo")); // false
// プロトタイプチェーンは見ない
console.log(Object.hasOwn(obj, "toString")); // false
7-2. in 演算子との違い
const obj = { name: "Kimura" };
console.log("name" in obj); // true
console.log("toString" in obj); // true ← プロトタイプチェーンを辿る!
console.log(Object.hasOwn(obj, "toString")); // false ← 自身のプロパティのみ
7-3. hasOwnProperty の古典的問題
// hasOwnPropertyは継承メソッドなので上書きされうる
const dangerous = {
hasOwnProperty: "I am a property",
realKey: 1,
};
// dangerous.hasOwnProperty("realKey"); // TypeError!
// Object.hasOwnなら安全
console.log(Object.hasOwn(dangerous, "realKey")); // true
// Object.create(null) のオブジェクトでも安全
const nullProto = Object.create(null);
nullProto.x = 1;
// nullProto.hasOwnProperty("x"); // TypeError(メソッドが存在しない)
console.log(Object.hasOwn(nullProto, "x")); // true ← OK
8. 分割代入を極める
分割代入(Destructuring)は、ES2015で追加されてからJavaScriptの書き味を一変させました。React開発では1日に何百回も書くパターンです。
8-1. 基本の分割代入
const user = { id: 1, name: "Kobayashi", email: "k@example.com" };
const { id, name, email } = user;
console.log(id, name, email); // 1 "Kobayashi" "k@example.com"
// 一部だけ取り出す
const { name: userName } = user;
console.log(userName); // "Kobayashi"
8-2. リネーム
const apiResponse = {
user_id: 1,
user_name: "Saito",
user_email: "s@example.com",
};
// snake_case → camelCase
const { user_id: userId, user_name: userName, user_email: email } = apiResponse;
console.log(userId, userName, email); // 1 "Saito" "s@example.com"
8-3. デフォルト値
function fetchUser({ id, page = 1, limit = 20 } = {}) {
return { id, page, limit };
}
console.log(fetchUser({ id: 100 }));
// { id: 100, page: 1, limit: 20 }
console.log(fetchUser({ id: 100, limit: 50 }));
// { id: 100, page: 1, limit: 50 }
console.log(fetchUser());
// { id: undefined, page: 1, limit: 20 }
8-4. リネーム + デフォルト値
const config = { host: "localhost" };
const { host: hostname = "0.0.0.0", port: portNumber = 3000 } = config;
console.log(hostname, portNumber); // "localhost" 3000
8-5. ネストの分割代入
const response = {
status: 200,
data: {
user: {
name: "Maeda",
address: { city: "Yokohama", zip: "220-0001" },
},
},
};
const {
data: {
user: {
name,
address: { city },
},
},
} = response;
console.log(name, city); // "Maeda" "Yokohama"
8-6. レスト構文で残りをまとめる
const user = { id: 1, name: "Murata", email: "m@example.com", role: "admin" };
const { id, ...rest } = user;
console.log(id); // 1
console.log(rest); // { name: "Murata", email: "m@example.com", role: "admin" }
// 特定プロパティを除外したコピーを作るイディオム
const { password, ...publicUser } = { id: 1, name: "X", password: "secret" };
console.log(publicUser); // { id: 1, name: "X" }
8-7. 関数引数での分割代入
type CreateUserOptions = {
name: string;
age?: number;
role?: "admin" | "user";
};
function createUser({ name, age = 20, role = "user" }: CreateUserOptions) {
return { name, age, role, createdAt: new Date() };
}
createUser({ name: "Hayashi" });
// { name: "Hayashi", age: 20, role: "user", createdAt: ... }
9. オプショナルチェイン と Nullish coalescing
ES2020で追加された?.と??は、深いネストを安全に辿るためのモダンJSの必須記法です。
9-1. オプショナルチェイン(?.)
const user = {
profile: {
address: null,
},
};
// 古い書き方
const city1 = user && user.profile && user.profile.address && user.profile.address.city;
// モダンな書き方
const city2 = user?.profile?.address?.city;
console.log(city2); // undefined ← エラーにならない
// メソッド呼び出しも対応
const result = obj?.maybeMethod?.();
// 配列インデックスも
const first = list?.[0];
9-2. Nullish coalescing(??)
// || はfalsy値全てに反応
const a = 0 || "default"; // "default" ← 0が消えてしまう
const b = "" || "default"; // "default" ← 空文字が消える
// ?? はnull / undefinedのみ
const c = 0 ?? "default"; // 0 ← 維持される
const d = "" ?? "default"; // ""
const e = null ?? "default"; // "default"
const f = undefined ?? "default"; // "default"
// 設定値のフォールバック
function init(options = {}) {
const timeout = options.timeout ?? 5000;
const retry = options.retry ?? 3;
return { timeout, retry };
}
console.log(init({ timeout: 0 })); // { timeout: 0, retry: 3 }
9-3. 組み合わせの実践
function getUserCity(user) {
return user?.profile?.address?.city ?? "未設定";
}
console.log(getUserCity({})); // "未設定"
console.log(getUserCity({ profile: { address: { city: "Sapporo" } } })); // "Sapporo"
console.log(getUserCity(null)); // "未設定"
10. Object.groupBy — ES2024の新機能
2024年にECMAScriptに追加されたObject.groupByは、配列要素をキーごとにグルーピングする待望のメソッドです。Node.js 21以降、ブラウザもChrome117+、Safari17.4+で利用可能です。
10-1. 基本の使い方
const products = [
{ name: "Apple", category: "fruit", price: 100 },
{ name: "Carrot", category: "vegetable", price: 80 },
{ name: "Banana", category: "fruit", price: 50 },
{ name: "Onion", category: "vegetable", price: 60 },
];
const grouped = Object.groupBy(products, (item) => item.category);
console.log(grouped);
/*
{
fruit: [
{ name: "Apple", category: "fruit", price: 100 },
{ name: "Banana", category: "fruit", price: 50 }
],
vegetable: [
{ name: "Carrot", category: "vegetable", price: 80 },
{ name: "Onion", category: "vegetable", price: 60 }
]
}
*/
10-2. 数値の範囲でグルーピング
const ages = [12, 25, 33, 45, 67, 18, 50];
const byAgeGroup = Object.groupBy(ages, (age) => {
if (age < 20) return "young";
if (age < 60) return "middle";
return "senior";
});
console.log(byAgeGroup);
// { young: [12, 18], middle: [25, 33, 45, 50], senior: [67] }
10-3. groupBy が無い環境のpolyfill
// 古い環境でも使えるシンプル実装
function groupBy(array, keyFn) {
return array.reduce((acc, item) => {
const key = keyFn(item);
(acc[key] ||= []).push(item);
return acc;
}, {});
}
const grouped = groupBy(
[{ type: "a" }, { type: "b" }, { type: "a" }],
(x) => x.type
);
console.log(grouped); // { a: [{type:"a"},{type:"a"}], b: [{type:"b"}] }
11. JSON との行き来
11-1. JSON.stringify の基本と整形
const data = { name: "Sasaki", tags: ["js", "ts"] };
console.log(JSON.stringify(data));
// {"name":"Sasaki","tags":["js","ts"]}
// 第3引数で整形(インデント幅)
console.log(JSON.stringify(data, null, 2));
/*
{
"name": "Sasaki",
"tags": [
"js",
"ts"
]
}
*/
11-2. replacer で出力をフィルタ
const user = {
id: 1,
name: "Watanabe",
password: "secret",
email: "w@example.com",
};
// 配列で指定 → そのキーだけ出力
console.log(JSON.stringify(user, ["id", "name"]));
// {"id":1,"name":"Watanabe"}
// 関数で指定 → 動的に制御
const safe = JSON.stringify(user, (key, value) => {
if (key === "password") return undefined;
return value;
});
console.log(safe);
// {"id":1,"name":"Watanabe","email":"w@example.com"}
11-3. reviver でパース時に変換
const json = '{"name":"Ito","birthday":"2000-01-15T00:00:00.000Z"}';
const parsed = JSON.parse(json, (key, value) => {
// ISO日付文字列をDateに復元
if (typeof value === "string" && /^d{4}-d{2}-d{2}T/.test(value)) {
return new Date(value);
}
return value;
});
console.log(parsed.birthday instanceof Date); // true
console.log(parsed.birthday.getFullYear()); // 2000
11-4. toJSON カスタマイズ
class User {
constructor(name, password) {
this.name = name;
this.password = password;
}
toJSON() {
// stringify時に呼ばれる
return { name: this.name }; // パスワードは出さない
}
}
const u = new User("Yoshida", "secret123");
console.log(JSON.stringify(u));
// {"name":"Yoshida"}
12. Map vs Object — どちらを使うべきか
「キー・値のペアを管理する」用途では、ObjectとMapの両方が選択肢になります。判断基準を整理します。
12-1. 使い分けの早見表
| 観点 | Object | Map |
|---|---|---|
| キーの型 | 文字列/Symbolのみ | 任意の型(オブジェクトもOK) |
| 順序保証 | 整数キーは昇順 | 挿入順を厳密に保持 |
| サイズ取得 | Object.keys().length | map.size |
| イテレート | for-in / entries | for-of(高速) |
| 頻繁な追加削除 | 遅め | 最適化されている |
| JSON化 | そのまま可能 | 変換が必要 |
| プロトタイプ衝突 | あり(toStringなど) | なし |
12-2. Mapの基本
const m = new Map();
m.set("name", "Goto");
m.set("age", 35);
m.set({ id: 1 }, "object as key!");
console.log(m.size); // 3
console.log(m.get("name")); // "Goto"
console.log(m.has("age")); // true
for (const [k, v] of m) {
console.log(k, v);
}
12-3. Object → Map 変換
const obj = { a: 1, b: 2, c: 3 };
const map = new Map(Object.entries(obj));
console.log(map.get("a")); // 1
console.log(map.size); // 3
12-4. Map → Object 変換
const map = new Map([
["x", 10],
["y", 20],
]);
const obj = Object.fromEntries(map);
console.log(obj); // { x: 10, y: 20 }
13. Immutable更新パターン — Reactの必須テクニック
React / Redux / Zustandなど、現代のフロントエンドでは「stateは直接書き換えず、新しいオブジェクトを返す」のが鉄則です。具体的なパターンを押さえましょう。
13-1. 1階層のプロパティ更新
const state = { count: 0, user: "Hara" };
// NG: 直接変更
// state.count = 1;
// OK: 新しいオブジェクトを生成
const newState = { ...state, count: state.count + 1 };
console.log(newState); // { count: 1, user: "Hara" }
console.log(state); // { count: 0, user: "Hara" } ← 不変
13-2. ネストされたオブジェクトの更新
const state = {
user: {
profile: {
name: "Hirano",
age: 30,
},
},
};
// 各階層でspreadが必要
const newState = {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
age: 31,
},
},
};
console.log(newState.user.profile.age); // 31
console.log(state.user.profile.age); // 30 ← 影響なし
13-3. 配列を含むオブジェクトの更新
const state = {
todos: [
{ id: 1, text: "buy milk", done: false },
{ id: 2, text: "write code", done: false },
],
};
// idで指定したtodoのdoneをtrueに
const updated = {
...state,
todos: state.todos.map((t) =>
t.id === 1 ? { ...t, done: true } : t
),
};
console.log(updated.todos[0].done); // true
console.log(state.todos[0].done); // false ← 不変
13-4. プロパティ削除のImmutable版
const state = { a: 1, b: 2, c: 3 };
// レスト構文で除外
const { b, ...rest } = state;
console.log(rest); // { a: 1, c: 3 }
console.log(state); // { a: 1, b: 2, c: 3 } ← 不変
14. Immer — Immutable更新を直感的に書く
ネストが深くなるとspreadは限界です。Immerを使えば、ミュータブルな書き方でImmutableな結果が得られます。Redux Toolkitの内部でも使われています。
14-1. Immerの基本
// npm install immer
import { produce } from "immer";
const state = {
user: {
profile: { name: "Nishi", age: 25 },
addresses: [{ city: "Tokyo" }],
},
};
const next = produce(state, (draft) => {
draft.user.profile.age = 26; // 普通の代入でOK
draft.user.addresses.push({ city: "Osaka" }); // pushもOK
});
console.log(next.user.profile.age); // 26
console.log(state.user.profile.age); // 25 ← 不変
console.log(next.user.addresses.length); // 2
14-2. Immerでカレント関数を作る
import { produce } from "immer";
// 状態を受け取って状態を返す関数を作れる
const addTodo = produce((draft, text) => {
draft.todos.push({ id: Date.now(), text, done: false });
});
const initial = { todos: [] };
const after = addTodo(initial, "buy bread");
console.log(after.todos); // [{ id: ..., text: "buy bread", done: false }]
14-3. ReduxToolkitでの内部利用
// Redux ToolkitのcreateSliceはinternalでImmerを使う
import { createSlice } from "@reduxjs/toolkit";
const userSlice = createSlice({
name: "user",
initialState: { name: "", age: 0 },
reducers: {
setName(state, action) {
// 直接書き換えてOK!Immerが面倒を見る
state.name = action.payload;
},
incrementAge(state) {
state.age += 1;
},
},
});
15. lodash の get / set — 動的なパス指定
キーがランタイムで決まる場合(設定ファイル、フォームライブラリなど)、lodashのget / setが便利です。
15-1. _.get で安全に値を取得
import _ from "lodash";
const obj = {
user: { profile: { address: { city: "Kobe" } } },
};
console.log(_.get(obj, "user.profile.address.city")); // "Kobe"
console.log(_.get(obj, "user.profile.foo.bar", "default")); // "default"
console.log(_.get(obj, ["user", "profile", "address", "city"])); // "Kobe"
15-2. _.set で深いプロパティをセット
import _ from "lodash";
const obj = {};
_.set(obj, "user.profile.name", "Mori");
console.log(obj); // { user: { profile: { name: "Mori" } } }
// 配列インデックスにも対応
_.set(obj, "tags[0]", "js");
_.set(obj, "tags[1]", "ts");
console.log(obj.tags); // ["js", "ts"]
15-3. _.cloneDeep の代替
import _ from "lodash";
const original = { date: new Date(), nested: { a: 1 } };
// structuredCloneでも対応できるが、lodashなら関数も保持できる
const clone = _.cloneDeep(original);
console.log(clone.date instanceof Date); // true
clone.nested.a = 999;
console.log(original.nested.a); // 1 ← 不変
16. TypeScriptでのオブジェクト型推論の限界
TypeScriptは強力ですが、Object操作では型推論が崩れる場面があります。回避策を押さえておきましょう。
16-1. Object.keysの戻り値はstring[]
type User = { id: number; name: string; age: number };
const user: User = { id: 1, name: "X", age: 20 };
// 期待: ("id" | "name" | "age")[]
// 実際: string[]
const keys = Object.keys(user);
// 型アサーションで対応
const typedKeys = Object.keys(user) as (keyof User)[];
typedKeys.forEach((k) => {
console.log(user[k]); // OK: k is "id" | "name" | "age"
});
16-2. Object.entriesも同様
function typedEntries<T extends object>(
obj: T
): [keyof T, T[keyof T]][] {
return Object.entries(obj) as [keyof T, T[keyof T]][];
}
const user = { id: 1, name: "Y" };
typedEntries(user).forEach(([k, v]) => {
// k: "id" | "name", v: number | string
console.log(k, v);
});
16-3. satisfies 演算子(TS 4.9+)
// 型を強制しつつ、リテラル型を保持
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
} satisfies Record<string, string | number>;
// config.apiUrl は string ではなく "https://api.example.com" 型
const url: "https://api.example.com" = config.apiUrl; // OK
17. パフォーマンス考察 — 何が速くて何が遅いか
「JavaScriptのオブジェクト操作で何が遅いのか?」は、本当のホットパスを書くまで意識する必要はありません。それでも知っておくと役立つベンチマーク事実があります。
17-1. 大量プロパティのコピーは遅い
// 10000プロパティを持つオブジェクトのコピー
function bench() {
const big = {};
for (let i = 0; i < 10000; i++) big[`k${i}`] = i;
console.time("spread");
for (let i = 0; i < 100; i++) ({ ...big });
console.timeEnd("spread");
console.time("Object.assign");
for (let i = 0; i < 100; i++) Object.assign({}, big);
console.timeEnd("Object.assign");
console.time("structuredClone");
for (let i = 0; i < 100; i++) structuredClone(big);
console.timeEnd("structuredClone");
}
bench();
// 一般にspread ≈ Object.assign < structuredClone(深いコピー対応のため重い)
17-2. キーが事前に分かっているなら直接アクセス
// ❌ 毎回Object.keysを回すのは無駄
function slowSum(obj) {
return Object.keys(obj).reduce((a, k) => a + obj[k], 0);
}
// ✅ 形が決まっているなら直接書く
function fastSum(obj) {
return obj.math + obj.english + obj.science;
}
17-3. 頻繁な追加・削除はMapが速い
const obj = {};
const map = new Map();
console.time("object set");
for (let i = 0; i < 1_000_000; i++) obj[i] = i;
console.timeEnd("object set");
console.time("map set");
for (let i = 0; i < 1_000_000; i++) map.set(i, i);
console.timeEnd("map set");
// Mapの方が10〜30%程度速い傾向
18. 現役エンジニアが本気で学ぶなら
ここまでで30以上のサンプルを通じて、Object操作の全領域をカバーしました。しかし、独学で身につけるべき範囲はこれだけではありません。React、Redux、TypeScript、テスト、設計パターンと、現代のフロントエンドエンジニアに求められる知識は膨大です。
もし「体系立てて、実務で通用するスキルを最短で身につけたい」と考えているなら、プロのカリキュラムで一気に学ぶ選択肢があります。以下のスクールは、いずれも無料カウンセリングや無料体験が用意されており、自分に合うかを試してから判断できます。
- テックアカデミー — 現役エンジニアのマンツーマンメンタリングが強み。フロントエンドコース・JavaScriptコースが充実。
- 侍エンジニア — 完全オーダーメイドカリキュラム。Reactや実務案件に直結する設計が可能。
- DMM WEBCAMP — 転職保証つきの社会人コース。未経験からの転職実績が業界トップクラス。
- レバテックカレッジ — 大学生・大学院生限定。短期集中で就活前にスキルを仕上げたい人向け。
18-1. 学習ロードマップの一例
1. JS基礎(変数・関数・スコープ・this)
2. ES2015+ のモダン構文(本記事の範囲)
3. 非同期(Promise / async・await)
4. DOM操作 + Fetch API
5. TypeScript(型システム・ジェネリクス)
6. React(関数コンポーネント・Hooks)
7. 状態管理(Zustand / Redux Toolkit)
8. テスト(Vitest / Testing Library)
9. ビルドツール(Vite / esbuild)
10. デプロイ(Vercel / Cloudflare Pages)
19. まとめ — 明日から使えるObject操作
長丁場おつかれさまでした。最後にこの記事で扱った要点を、6つの実務指針として整理します。
- Object.entries + fromEntries でオブジェクトのmap / filterを表現する
- structuredClone を深いコピーの第一選択肢にする(JSON経由は捨てる)
- Object.hasOwn をプロパティ存在チェックの標準とする
- ?. と ?? でネストとnullを安全に処理する
- spread or Immer でImmutable更新を徹底する
- 頻繁な追加削除や非文字列キーがあるならMapを選ぶ
本記事のコードはすべてES2025対応の最新版です。お気に入りのプレイグラウンド(PlayCodeやStackBlitz)に貼って、ぜひ手を動かして確認してみてください。Object操作を制すれば、React / TypeScript / Node.jsすべての世界で書ける幅が広がります。
関連記事として、JavaScript配列メソッド完全ガイドやasync/await完全実践ガイドもあわせてどうぞ。

コメント