0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

この記事でわかること

  • 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

Set1個ずつ順に取り出せる 性質(反復可能といいます)があります。配列と同じように for...of が使えます。

const fruits = new Set(["apple", "banana"]);

for (const name of fruits) {
  console.log(name);
}

forEach を使うときの注意

配列にも forEach がありますが、SetforEach では 第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つです。

NaNSet の中では「同じ」とみなされる

普通の === では NaN === NaNfalse ですが、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>()<>「中身の型の指定」(ジェネリクス)
  • **「同じ値か」**の細かいルールの名前が SameValueZeroNaN0 / -0、オブジェクトの参照で挙動が変わるので、困ったらこの記事の該当箇所を見返すとよいです
  • 一覧 × 別リストの照合 は、Set にしてから has するパターンがよく使われます

まずは Playground で、addhas → スプレッドで配列に戻す、の3つを触ってみると理解が進みやすいです。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?