タタッと!ドラゴンさん🐉

タタッ!と書いてく記事まとめ

【TypeScript↗️】いつ「number がキーになる」になるの👀?

🐉 グォォォオオ! Hello,World!! ドラゴンだ!

🐉ちなみに好きな英単語の間に数字入れて略すワードは「i18n (internationalization : 国際化)」だぜ!  k8sとかa16zとかどことなく厨二心をくすぐられる感じがいいよネ!

🐉 ...

🐉 ...(ちなみにS3みたいな略し方も好き )

k8s : プロダクショングレードのコンテナ管理基盤 - Kubernetesの略 ※ a16z : 米国を代表するトップクラスのVC、Andreessen Horowitz の略 ※ S3 : Amazonが提供するオブジェクトストレージサービス、Amazon Simple Storage Service の略

🐉今回はTypeScript書籍を読んでいて気になった「数値をキーに持つってケース結局いつなんだ?」ってことについてです!

🐉基本的に数値をキーに持つというコードはかかないため、そもそもキーに持てることを知らない人も多いかもしれません。筆者もへぇーと思いましたが、せっかくなので登場するケースを予習して、もう一歩踏み込んでみたいと思いました。

TypeScript における「number がキーになる」ケースの使い分け

TypeScript で数値を扱う方法は、配列やオブジェクトなど複数ありますが、具体的にどのように使い分けるとよいのでしょうか? 本記事では、

  • インデックス操作(配列)
  • タプル (Tuple)
  • number がキーになるオブジェクト(Index Signature)

これらの違いと、どんなシーンで役立つのかを整理していきます。「存在は知っているけれど、実際どのときに使うのかピンとこない……」という方の参考になれば幸いです。


概要:知ってはいるが、いつ使うのか分からない?

TypeScript で配列やオブジェクトを扱うとき、

  1. インデックス操作 (array[index] で要素を取得する方法)
  2. タプル (tuple) ([number, string] のように要素数や型が固定された配列型)
  3. number がキーになるオブジェクト ({ [key: number]: string } と型定義する方法)

など、数値を扱う方法は複数存在します。一見どれも似ているように見えますが、

  • 「わざわざ number キーを使うメリットはあるの?」
  • 「配列やタプルでよくないの?」

と感じる方も多いでしょう。そこで本記事では、まずそれぞれが何を意味するのかを整理し、どのような場面で使い分けるべきかを具体的に解説していきます。


1. そもそも「インデックス操作」「タプル」「number キーのオブジェクト」とは

1-1. インデックス操作

JavaScript でおなじみの「配列に対して array[index] でアクセスする」方法です。TypeScript では型チェックが入る点が異なりますが、基本的な使い方は同じです。

const numbers = [10, 20, 30];  
// インデックス操作
const first = numbers[0]; // number
  • メリット

    • JavaScript の標準的な配列操作なので、直感的に理解しやすい
    • 素数が可変な場合でも簡単に扱える
  • デメリット

    • 素数が固定ではないため、特定の位置の要素型を厳密に保証しづらい
    • numbers[10] のように存在しないインデックスを指定すると undefined が返る可能性がある

1-2. タプル (Tuple)

タプルは「配列の要素数や各要素の型が固定されている配列型」です。例えば [number, string] であれば、1番目は number、2番目は string といった具合に厳密に定義できます。

type UserData = [number, string];
// 1番目: userId (number), 2番目: userName (string)

const user: UserData = [1, 'Alice'];  
// OK

const invalidUser: UserData = ['Bob', 2];  
// エラー: 型が合わない
  • メリット

    • 素数や型をコンパイラが厳密に保証してくれる
    • 配列ほど大きな構造ではないが、しっかりと要素型を定義したいときに最適
  • デメリット

    • 素数が固定なので、後から要素を増やしたり削除したりする操作には不向き
    • 要素が増えると可読性が下がる

1-3. number がキーになるオブジェクト (Index Signature)

TypeScript では次のように、[key: number]: string; と記述することで、数値をプロパティキーとするオブジェクトを定義できます。

type NumberKeyObject = {
  [key: number]: string;
};

const example: NumberKeyObject = {
  0: 'Alice',
  1: 'Bob',
  // ...
};

一見、配列と似ていますが、「オブジェクトのプロパティとして number を使う」 点で異なります。JavaScript の仕様上、実際には数値のキーは文字列化されるため、example[0]example["0"] と解釈されます。それでも TypeScript の型システム上、「number でアクセスするオブジェクト」 と表現できるのがポイントです。

  • メリット

    • “キーが数値” というオブジェクト構造を型で表現できる
    • 連想配列のように柔軟に追加情報を持たせることが可能
  • デメリット

    • 実際にはキーは文字列として扱われるため、配列のように連続しているわけではない
    • 存在しないキーを指定すると undefined が返ってくる可能性がある

2. 似ているけれど、どのように使い分ける?

数値でアクセスするという点ではどれも似ていますが、

  1. 配列が必要なのか?(要素数は可変か固定か)
  2. 順番と各インデックスの意味が厳密に決まっているか?
  3. オブジェクト(連想配列)として管理したいのか?

といった観点で使い分けるのがおすすめです。以下では各ケースをコード例とともに見ていきます。

2-1. インデックス操作(配列)を使うべきケース

  • 素数が可変
  • 順番が重要で、要素が連続している
  • JavaScript の標準メソッド(map, filter, reduce 等)を活用したい

例えば「ユーザー ID を順番に管理する」「時系列でイベントを追加していく」など、順番が重要な場面では配列がもっとも自然です。

// 例: ユーザーIDの配列
const userIds: number[] = [1, 2, 3];

// 配列末尾に要素追加
userIds.push(4); // [1, 2, 3, 4]

// インデックスでアクセス
console.log(userIds[0]); // 1

// 標準的な配列メソッドを利用
const filteredUserIds = userIds.filter(id => id !== 2);
console.log(filteredUserIds); // [1, 3, 4]

配列の利点は「要素が連続していて順序が大事」な場面に適していること。一方、存在しないインデックスにアクセスすると undefined になる点に注意が必要です。

2-2. タプル (Tuple) を使うべきケース

  • 素数が固定
  • 「順番」と「型」が密接に関連している

例えば「(userId, userName) の 2 要素だけを確実にセットにしたい」「(x, y, z) のように座標を 3 要素で扱いたい」場合はタプルが最適です。

// (userId, userName) のタプル型を定義
type UserData = [number, string];

const user: UserData = [1, 'Alice'];  
// OK

// エラー例: 順番と型が合わない
const invalidUser: UserData = ['Alice', 1];
// Type 'string' is not assignable to type 'number'

タプルなら「先頭は必ず number、次は string」というように、コンパイラが要素ごとの型を厳密にチェックしてくれます。ただし、要素が多くなりすぎると可読性が落ちるため、タプルは「要素数が少なく、明確な順番と型が必要」な場面で使うのが理想です。

2-3. number がキーになるオブジェクトを使うべきケース

  • オブジェクトとして管理しつつ、キーには数値を使いたい
  • 配列の連続的な性質が不要
  • 座席番号や商品コードのように「欠番」や「ランダムな番号」が発生しうる

例えば座席番号をキーにして、その席に座っている人の情報を格納したい場合、配列に連続的に詰めることもできます。しかし「1番・5番だけ埋まっていて、2〜4番は空席」といった欠番シナリオでは、number キーのオブジェクトのほうが自然に表現できます。

2-3-1. 座席管理の例

type SeatInfo = {
  occupantName: string;
  isVip: boolean;
};

type SeatMap = {
  [seatNumber: number]: SeatInfo;
};

const seats: SeatMap = {
  1: { occupantName: 'Alice', isVip: false },
  5: { occupantName: 'Bob', isVip: true },
  // 連続していない seatNumber でもOK
};

// 存在しない seatNumber へのアクセスは undefined
console.log(seats[2]); // undefined

2-3-2. スコアボードの例

// プレイヤーIDをキーにしてスコアを格納
type PlayerScoreBoard = {
  [playerId: number]: number;
};

const scores: PlayerScoreBoard = {};

// プレイヤーID 10 番にスコア 50 を設定
scores[10] = 50;
// プレイヤーID 12 番にスコア 120 を設定
scores[12] = 120;

console.log(scores[10]);  // 50
console.log(scores[999]); // undefined

2-3-3. 商品管理の例

type Product = {
  name: string;
  price: number;
};

type ProductMap = {
  [productCode: number]: Product;
};

const products: ProductMap = {
  1001: { name: 'Apple', price: 100 },
  2002: { name: 'Banana', price: 80 },
};

// 数値キーなので productCode 毎に管理しやすい
console.log(products[1001].name); // "Apple"

配列でも同様の管理は可能ですが、「number をキーとしてデータを持つ」 という意図が明確な場合は、Index Signature を用いたオブジェクトのほうがより直感的になります。


3. まとめ:選択のポイント

TypeScript で「number をキーにする」方法は複数ありますが、それぞれ使いどころが異なります。ポイントをまとめると以下のとおりです。

  1. インデックス操作(配列)

    • 素数が可変で連続的な並びを扱いたい
    • 例: ユーザー ID のリスト、時系列のイベントログ
  2. タプル (Tuple)

    • 素数が固定で、各要素の型や順番が明確に決まっている
    • 例: (userId, userName)(x, y, z) のように要素の型が厳密に定義される
  3. number キーのオブジェクト (Index Signature)

    • 連続性は不要だが、数値をキーにした連想配列のような構造を使いたい
    • 例: 座席番号、プレイヤーID、商品コードなど、欠番やランダムな番号が発生しうる場合

「わざわざ number キーを使うメリットは?」と思うかもしれませんが、配列とは異なる制約や管理しやすさを得られるのが強みです。どれを使うべきか迷ったら、

  • 素数は可変か固定か?
  • 順序と各インデックス(要素)の型が厳密に決まっているか?
  • それとも連想配列として自由度の高い管理が必要か?

といった観点で検討しましょう。きっと これでTypeScript でのデータ扱いがさらに捗るはずです。