web-utilities-native-js について
JavaScript と TypeScript の開発者向けに、ユーティリティライブラリの代わりとなるネイティブ API の使い方と、複製や不変更新の注意点をまとめたスキルです。
主な機能
- 01ライブラリ関数のネイティブな代替
- 02structuredClone による深い複製
- 03不変な配列更新
- 04配列のグループ化と Set 演算
- 05オブジェクト変換と関数ユーティリティ
活用シーン
- 01ユーティリティライブラリへの依存を減らす
- 02日付やコレクションを含むデータの安全な複製
- 03共有配列を不変な形で更新する
- 04キーごとにデータを分類し集合演算を行う
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-utilities-native-jsインストール後、Claude Code で/web-utilities-native-jsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.yamlreference.mdexamples/arrays.mdcore.mdobjects.md詳細
ネイティブ JavaScript のユーティリティパターン
クイックガイド: ほとんどのユーティリティライブラリの呼び出しには、バンドルサイズを増やさないネイティブな代替手段があります。 コードの書き方を変えるのは、ディープコピーの
structuredClone、ES2023 の不変配列メソッド(toSorted、toReversed、toSpliced、with)、Object.groupBy/Map.groupBy、そして ES2025 の Set 演算です。見落としやすい点は 2 つあります。structured clone はプロトタイプを破棄し、関数に対して例外をスローします。また、不変配列メソッドがコピーするのは 1 階層だけです。
詳細なリソース:
- examples/core.md — クローン、不変な更新、負のインデックス、末尾からの検索
- examples/arrays.md — 重複排除、集合演算、グループ化、チャンク化、ソート、不要な値の除去
- examples/objects.md — 型付きの pick/omit/mapValues、ディープマージ、ルックアップマップ、凍結
- reference.md — lodash とネイティブ機能の対照表、関数ユーティリティ(debounce、throttle、memoize)、利用可能状況
<critical_requirements>
ユーティリティコードを書く前に
依存関係を追加する前に、まずネイティブメソッドを使ってください。 reference.md の lodash とネイティブ機能の対照表を見れば、ほとんどのケースはすぐに判断でき、ネイティブの呼び出しで追加されるものはありません。
ディープコピーには structuredClone を使ってください。 JSON の往復変換では Date が文字列になり、Map や
Set は {} になります。しかも、それは何も通知されないまま起きるため、最初の .getTime() で失敗するまで問題が表面化しません。
配列を共有している場合は to* 配列メソッドを使ってください。 sort、reverse、splice は配列をその場で書き換えます。そのため、sort を使って「ソート済みのコピーを返す」関数は、呼び出し元のデータを変更しています。
メンバーシップを繰り返し確認する前に、Set に変換してください。 filter の中で includes を使うと計算量は O(n²) になり、数千要素ほどになると差が現れます。
</critical_requirements>
自動検出: structuredClone、Object.groupBy、Map.groupBy、toSorted、toReversed、toSpliced、 Array.prototype.with、findLast、findLastIndex、at(-1)、Object.hasOwn、Object.fromEntries、 Set.prototype.union、Set.prototype.intersection、Set.prototype.difference、symmetricDifference、 isSubsetOf、isDisjointFrom、lodash の代替、ネイティブ JavaScript ユーティリティ
対象:
- 配列操作 — 重複排除、グループ化、チャンク化、平坦化、不変なソート、末尾からの検索
- オブジェクト操作 — 抽出、省略、マージ、キー/値の変換、ルックアップマップ
- Set の代数演算 — 和集合、積集合、差集合、部分集合の判定
- 依存関係を追加せずに行うディープコピー
- 手書きの関数ユーティリティ — debounce、throttle、memoize、once
別の項目で扱う内容:
- 日付の計算、解析、書式設定 —
Dateだけでは対応できません - リアクティブな更新 — これらのメソッドは新しい参照を生成します。新しい参照を何が利用するかは、状態を所有する側の問題です
- スキーマ検証と実行時の解析 — これらのユーティリティは、すでに信頼できるデータを変換します
<decision_framework>
ネイティブ機能か、依存関係か
次の順に、3 つのケースを検討してください。
- ネイティブメソッドがある — それを使ってください。reference.md の表で確認できます。
- ネイティブメソッドはないが、実装が 10 行で済む — 自分で書いてください。
chunk、pick、omit、compact、deepMerge、debounce、throttle、memoize、onceはすべて、このスキルで型付きの実装を紹介しています。 - エッジケースこそが機能である — 依存関係を導入してください。
cancel、flush、先行/後続エッジに対応した debounce、キーごとの配列戦略に対応したディープマージ、中間配列が問題になるほど大規模なデータセットに対する遅延評価などが該当します。こうした機能を不適切に再実装するコストは、バイト数を上回ります。
</decision_framework>
基本パターン
パターン 1:ディープコピーには structuredClone
Date、Map、Set、RegExp、型付き配列、循環参照を保持します。これらはすべて、JSON の往復変換では失われます。
const snapshot = structuredClone(state);
// Preserved: Date, Map, Set, RegExp, ArrayBuffer, circular refs
// Throws on: functions, DOM nodes, Proxy, symbols
// Silently lost: the prototype — a class instance clones to a plain object
コード全文:examples/core.md
パターン 2:イミュータブルな配列メソッド(ES2023)
4 つの to* メソッドは、従来のメソッドが元の配列を書き換えていたのに対し、新しい配列を返します。
items.toSorted((a, b) => a.price - b.price); // vs .sort()
items.toReversed(); // vs .reverse()
items.toSpliced(index, 1); // vs .splice()
items.with(index, { ...items[index], done: true }); // vs items[i] = x
コピーは浅いコピーです。新しい配列が同じ要素オブジェクトを保持するため、上記の更新では書き込む前にスプレッド構文を使用しています。
完全なコード:examples/core.md
パターン 3:Object.groupBy と Map.groupBy(ES2024)
const byStatus = Object.groupBy(orders, (order) => order.status);
// { pending: [...], shipped: [...] } — null-prototype, values are `T[] | undefined`
const byCustomer = Map.groupBy(orders, (order) => order.customer);
// Map keyed by the object itself — use this when the key is not a string
Object.groupBy はキーを文字列に変換して、プロトタイプが null のオブジェクトを返します。そのため、byStatus.hasOwnProperty(key) ではなく Object.hasOwn(byStatus, key) を使ってください。
完全なコード:examples/arrays.md
パターン 4:Set の演算(ES2025)
setA.union(setB);
setA.intersection(setB);
setA.difference(setB); // in A, not in B
setA.symmetricDifference(setB);
setA.isSubsetOf(setB);
setA.isSupersetOf(setB);
setA.isDisjointFrom(setB);
いずれも Set を返すため、配列が必要な場合はスプレッド構文を使います。引数には size、has、keys を備えたセット風のオブジェクトを指定できます。Map は該当しますが、通常の配列は該当しません。
完全なコード:examples/arrays.md
パターン 5:末尾からの検索とインデックス指定
logs.findLast((log) => log.level === "error"); // ES2023, single pass
logs.findLastIndex((log) => log.level === "error");
items.at(-1); // ES2022, undefined when empty
"file.backup.txt".split(".").at(-1); // "txt" — strings too
完全なコード:examples/core.md
パターン 6:変換の要となる Object.fromEntries
entries → map/filter → fromEntries という形で、pick、omit、invert、mapValues、mapKeys、配列からルックアップへの変換を網羅できるため、これらのヘルパーは依存関係なしでそれぞれ 4 行で実装できます。
const keyed = Object.fromEntries(items.map((item) => [item.id, item]));
const withTax = Object.fromEntries(
Object.entries(prices).map(([k, v]) => [k, v * 1.1]),
);
const passing = Object.fromEntries(
Object.entries(scores).filter(([, score]) => score >= 90),
);
完全なコード:examples/objects.md
<red_flags>
注意すべき点
実行時にエラーになるもの:
- 関数、DOM ノード、Proxy を含むものに対して
structuredCloneを使うとDataCloneErrorが発生します。データと振る舞いは分けて複製してください。 JSON.parse(JSON.stringify(x))はエラーにはなりませんが、データを壊します。Dateは文字列になり、MapやSetは{}になり、undefinedのメンバーは消えます。その結果に対する最初のメソッド呼び出しで問題が表面化します。Object.groupByはプロトタイプが null のオブジェクトを返すため、result.hasOwnProperty(...)を呼び出すとエラーになります。Object.hasOwnを使ってください。- Set のメソッドは通常の配列を受け付けません。
size、has、keysを備えたものが必要です。
予想外の動作:
structuredCloneはプロトタイプを破棄します。クラスのインスタンスを複製すると、同じフィールドを持ちながらメソッドのない通常のオブジェクトになります。- 比較関数を指定せずに
toSorted()を呼び出すと、文字列としてソートされます。そのため、[10, 9, 1].toSorted()は[1, 10, 9]になります。これはsort()が常に行ってきた動作と同じです。 to*メソッドによるコピーは 1 階層までです。新しい配列の要素を編集すると、元の配列にある同じオブジェクトも編集されます。arr.at(-1)の型は、コンパイラーオプションにかかわらずT | undefinedです。そのため、空の場合に対処する必要があります。arr[arr.length - 1]の型はnoUncheckedIndexedAccessが有効でない限りTです。チェックされていない状態でundefinedが流れ込むのを許してしまうのは、この形式です。Errorに対するstructuredCloneは成功し、標準フィールドは保持されますが、code、status、その他追加されたプロパティはコピーされません。filter(Boolean)は nullish 値に加え、0、""、falseも除外します。また、TypeScript は結果の型を絞り込みません。filter((x): x is T => x != null)を使えば、どちらの問題にも正しく対処できます。Object.freezeは浅い処理です。また、strict モード以外では、凍結されたオブジェクトへの書き込みはエラーにならず、黙って失敗します。Object.keysは挿入順にかかわらず、整数のようなキーを数値の昇順で先に返します。挿入順が保たれるのは文字列キーだけです。
</red_flags>