HOWTO · JavaScript

JavaScript のタプル代替:配列、オブジェクト、TypeScript

JavaScript にネイティブのタプル型はありません。配列、オブジェクト、Object.freeze()、分割代入、TypeScript のタプルを用途別に使い分けます。

このページの内容

JavaScript には、実行時に使えるネイティブのタプル型はありません。座標のような短い順序付きデータには配列を使い、分割代入で値を取り出します。名前で意味を示す方が分かりやすい場合はオブジェクトを使います。実行時の直接変更を防ぐには配列を凍結し、開発時に長さと要素型を検査するには TypeScript のタプルを使います。

順序付きの値には配列を使う

各位置の意味が固定されている場合、配列をタプルのように扱えます。次の関数は最小値、最大値の順で返します。

function minMax(values) {
  return [Math.min(...values), Math.max(...values)];
}

const [minimum, maximum] = minMax([7, 3, 12]);
console.log(minimum, maximum);

出力:

3 12

分割代入によって各位置に意味のあるローカル名を付けられます。呼び出し側のために順序を明記してください。JavaScript は配列の長さや要素型を強制しないため、外部データを位置ベースの契約として使う前に検証が必要です。

実行時の変更を防ぐ必要がある場合だけ配列を凍結する

const が防ぐのは変数の再代入であり、その変数が参照する配列の変更ではありません。Object.freeze() は配列自体を凍結します。

"use strict";

const point = Object.freeze([48.8566, 2.3522]);

console.log(point[0], point[1], Object.isFrozen(point));
// point[0] = 0; // TypeError in strict mode
// point.push(3); // TypeError

出力:

48.8566 2.3522 true

Object.freeze() はコピーを作らず、渡された同じオブジェクトを返します。入力配列を呼び出し側も所有している場合は、予期せず相手の配列を凍結しないようコピーしてから処理します。

function asFrozenPoint(value) {
  if (
    !Array.isArray(value) ||
    value.length !== 2 ||
    !value.every(Number.isFinite)
  ) {
    throw new TypeError("Expected two finite coordinates");
  }

  return Object.freeze([...value]);
}

凍結は浅い処理です。配列の直下の要素、長さ、プロパティは変更できなくなりますが、要素として格納されたオブジェクトは変更できます。

const settings = Object.freeze([{ theme: "light" }]);

settings[0].theme = "dark";
console.log(settings[0].theme);

出力:

dark

したがって、浅く凍結した入れ子構造を完全に不変とは呼べません。深い凍結には入れ子の走査や循環参照への対応が必要なので、その保証が本当に必要な場合にだけ導入します。

位置より名前が明確ならオブジェクトを使う

複数の文字列や真偽値、省略可能な項目、将来増える項目を位置だけで表すと、呼び出し側が意味を取り違えやすくなります。

function createUser() {
  return { id: 42, name: "Ada", active: true };
}

const { id, name, active } = createUser();

[x, y]Object.entries() が返す [key, value] のような定着した短い組には配列が適します。各インデックスの意味を覚える必要があるなら、名前付きプロパティを持つオブジェクトの方が安全です。

Object.entries() は文書化された位置付きの組を返します。分割代入を使うと、使う場所で二つの位置を明確にできます。

for (const [key, value] of Object.entries({ theme: "light" })) {
  console.log(`${key}: ${value}`);
}

出力:

theme: light

型を検査するには TypeScript のタプルを使う

TypeScript は長さ、順序、要素型を型として表現できます。

const point: readonly [number, number] = [48.8566, 2.3522];

readonly は TypeScript ソース上の書き込みを禁止します。コンパイル後は通常の JavaScript 配列であり、実行時には凍結されません。静的検査と実行時保護の両方が必要な場合だけ、TypeScript のタプルと Object.freeze() を組み合わせます。

まとめ

JavaScript の配列は規約によってタプルのように使えます。名前付きデータにはオブジェクト、浅い実行時保護には Object.freeze()、長さと型の静的検査には TypeScript のタプルを選びます。