この記事でわかること
-
Setが 配列とどう違うか -
add/has/sizeなど、まず覚えるとよい操作 -
「同じ値」の決め方(
SameValueZeroという言葉の正体と、NaNやオブジェクトでハマる話) - レシピ一覧にお気に入りフラグを付ける 流れ
動作確認におすすめのサイト
ブラウザ上で TypeScript をそのまま試せます。
配列と Set のちがい(イメージ)
配列 []
|
Set |
|
|---|---|---|
| 役割のイメージ | 順番付きのリスト。0番目、1番目…と番号で取り出す | 「入っている値」のかたまり。同じ値は1回だけ |
| 重複 | そのままなら 同じ値が何回でも入る | 同じ値は自動で1つ にまとまる |
| 「この値がある?」 |
arr.includes(値) など |
set.has(値) |
どちらが「正しい」わけではなく、やりたいことに合わせて使い分けます。
簡単なコードで動作確認してみる
空の Set を作って、文字を入れて、入っているか調べます。
const tags = new Set<string>();
tags.add("js");
tags.add("ts");
tags.add("js"); // もう一度「js」→ 中身は変わらない(重複しない)
console.log(tags.has("js")); // true
console.log(tags.has("css")); // false
console.log(tags.size); // 2(js と ts)
size は「いまいくつ値が入っているか」です。
よく使う操作
Setの作成・追加
// 空から
const a = new Set<number>();
a.add(10);
console.log(a.has(10)); // true
// 最初から値を渡す(配列から作るイメージ)
const b = new Set([1, 2, 2]);
console.log(b.size); // 2(2 が重ならない)
Set内要素の削除
const s = new Set(["a", "b"]);
console.log(s.delete("a")); // true(消せた)
console.log(s.has("a")); // false
s.clear(); // 空っぽにする
console.log(s.size); // 0
配列変換・重複削除
...(スプレッド)で、中身を配列に広げられます。
const scores = [80, 90, 80, 70];
// 重複削除
const unique = [...new Set(scores)];
console.log(unique); // [ 80, 90, 70 ]
「一度 Set に通すと重複が落ちる」→「配列に戻す」、という流れです。
中身をひとつずつ確認(for...of)
Set は 1個ずつ順に取り出せる 性質(反復可能といいます)があります。配列と同じように for...of が使えます。
const fruits = new Set(["apple", "banana"]);
for (const name of fruits) {
console.log(name);
}
forEach を使うときの注意
配列にも forEach がありますが、Set の forEach では 第2引数は「インデックス」ではなく、第1引数と同じ値 が入ります(Set に「0番目」という番号がないため)。
new Set(["x","y"]).forEach((first, second) => {
console.log(first === second); // true
console.log(first + second); // xx yy
});
TypeScript の <> は何?(ジェネリクス)
ジェネリクスはこのSetを学ぶ上でも重要な要素なので復習しておきましょう。
new Set<string>() の <string> は、TypeScript 特有の書き方です。
ジェネリクス(ジェネリック) とは、ざっくり言うと 「この箱には、どんな型の値を入れてよいか」を <> の中で指定する 仕組みです。
-
new Set<string>()→ 文字列だけ入れてよいSet -
new Set<number>()→ 数値だけ入れてよいSet
<> を書かなくても、初期値から TypeScript が推測してくれることも多いですが、明示しておく方が開発者にとって親切なので、明示しておくケースが多いです。
const inferred = new Set([1, 2, 3]); // 数の Set だと推論される
console.log([...inferred]);
オブジェクトを入れるとき
Set が「同じかどうか」見るのは、オブジェクトの 中身 ではなく 同じかたまり(参照)かどうか です。字面が同じ { id: 1 } を2回 add しても、別のかたまりとして 2つ入ります。
const box = new Set<object>();
const one = { id: 1 };
box.add(one);
box.add(one); // さっきと同じ one
console.log(box.size); // 1
box.add({ id: 1 }); // 新しいかたまり
console.log(box.size); // 2
どんなときに Set を使うとよいか
次のようなときは、Set を思い出すとよいです。
- 配列から重複を取り除きたい(上のスプレッドの例)
- 「この ID は許可されているか」 のように、同じチェックを何度もする
- 一覧の各行に「別の ID のリストに入っているか」フラグを付けたい(次の実践編)
逆に、0番目・1番目…と番号で何度も触りたいなら、配列のほうが向いていることが多いです。
「同じ値」はどう決まる?(SameValueZero って?)
Set が「もう入っているから追加しない」ときなど、2つの値が同じかどうかを内部で決めています。そのルールの名前が、仕様では SameValueZero と呼ばれています。
覚える必要はありません。「仕様書や記事でこの言葉を見たら、ただの“等しいか判定のルールの名前”だ」 と思ってください。
実務でつまずきやすいのは次の2つです。
NaN は Set の中では「同じ」とみなされる
普通の === では NaN === NaN は false ですが、Set では NaN は1つにまとまります。
const s = new Set<number>();
s.add(NaN);
s.add(NaN);
console.log(s.size); // 1
console.log(NaN === NaN); // false(=== とはちがう)
そもそも NaN とは?
NaN(ナン)は Not a Number(「数として表せない結果」)の略です。たとえば 文字を無理やり数にしたけれど失敗した ときに出ます。
console.log(Number("hello")); // NaN
console.log(Number("123")); // 123(これは普通の数)
型は number の一種として扱われますが、「有効な実数の値」ではない という特別な値です。だから「NaN かどうか」を調べるときは === ではなく Number.isNaN(値) を使う、という書き方が推奨されることが多いです。
const x = Number("oops");
console.log(Number.isNaN(x)); // true
0 と -0 は「同じ」とみなされる
const s = new Set<number>();
s.add(0);
s.add(-0);
console.log(s.size); // 1
取り出す順番は「入れた順」
const s = new Set<string>();
s.add("いち");
s.add("に");
s.add("さん");
console.log([...s]); // [ 'いち', 'に', 'さん' ]
実践編:レシピ一覧にお気に入りフラグを付ける
やりたいことだけ先に書きます。
- レシピの一覧(それぞれに
idがある)がある - お気に入りに入っているレシピの ID の一覧 もある
- 各レシピに
isFavorited(お気に入りかどうか) を付けたい
ストーリーで動かしてみる
まずは API や async を使わない形で、心の中の動きと同じことをします。
// 検索結果のレシピ(例)
const recipes = [
{ id: "r1", title: "カレー" },
{ id: "r2", title: "パスタ" },
{ id: "r3", title: "サラダ" },
];
// お気に入りに入っているレシピの ID(同じ ID が二重でもよい)
const favoriteIds = ["r2", "r3", "r3"];
// お気に入り ID を Set にする → 「この ID はお気に入り?」がすぐ調べられる
const favorites = new Set(favoriteIds);
// 各レシピの id が、お気に入り ID の集合に含まれるか
const withFlag = recipes.map((recipe) => ({
...recipe,
isFavorited: favorites.has(recipe.id),
}));
console.log(favorites.size); // 2(r3 の重複は1つにまとまる)
console.log(withFlag);
// r1 は false、r2 と r3 は true
なぜ Set か を言葉だけで言うと次のとおりです。
- お気に入り ID が 配列のままだと、各レシピごとに
favoriteIds.includes(recipe.id)のように 配列を先頭からなめる 形になりがちです。レシピが増え、お気に入りも増えると、そのたびに なめる回数も増えやすい です。 - 一度
Setにしておくと、favorites.has(recipe.id)で「入っているか」を調べられます。こちらのほうが、だいたい速くて読みやすい ことが多いです。
まとめ
-
Setは 重複のない値のかたまり。hasで「入っているか」を調べやすい - TypeScript の
new Set<string>()の<>は 「中身の型の指定」(ジェネリクス) - **「同じ値か」**の細かいルールの名前が
SameValueZero。NaNや0/-0、オブジェクトの参照で挙動が変わるので、困ったらこの記事の該当箇所を見返すとよいです -
一覧 × 別リストの照合 は、
Setにしてからhasするパターンがよく使われます
まずは Playground で、add → has → スプレッドで配列に戻す、の3つを触ってみると理解が進みやすいです。