こんにちは!TypeScriptの世界へようこそ。
フロントエンドからバックエンドまで、型という強力な武器を使って堅牢なアプリケーションを構築していく上で、今日のテーマは避けて通れない非常に重要なステップです。
他の言語(PythonやGo、Rustなど)からTypeScriptに入ってきた方の中には、「関数から複数の値を返したいときはどうすればいいんだろう?」と疑問に思った方もいるのではないでしょうか。オブジェクトをわざわざ定義するほどでもないけれど、2つ以上の値をキレイに、かつ型安全に取り扱いたい。
そんなときにお勧めしたいのが、「タプル型による多値戻り値のハンドリング」です。
ここをクリアすれば、TypeScriptの型推論の仕組みと、配列・タプルの本質がぐっと身近になりますよ。一緒に楽しくマスターしていきましょう!
—
1. なぜ「オブジェクト」ではなく「タプル」なのか?
複数の値を返す関数を作るとき、JavaScript/TypeScriptでは真っ先にオブジェクトを思い浮かべるはずです。
// 良くあるオブジェクトを返すアプローチ
function fetchUser(id: number) {
// 処理…
return { name: “Taro”, age: 25 };
}
const user = fetchUser(1);
console.log(user.name, user.age);
この方法はもちろん安全ですが、少し大げさだと感じる場面はありませんか?
例えば、関数名や文脈から「何を返すかが一目瞭然」な場合や、一時的な計算結果のペア(座標の `[x, y]` や、成否とエラーの `[result, error]` など)を返す場合です。
タプル型(Tuple)とは?
TypeScriptのタプル型は、一言で言うと「要素数が固定され、それぞれの位置(インデックス)の型が厳密に定義された配列」です。
イメージとしては、こんなロッカーを思い浮かべてみてください。
[ 0番目のロッカー: string型専用 ] -> “Taro”
[ 1番目のロッカー: number型専用 ] -> 25
普通の配列(`string[]` など)が「何個入ってもいいし、中身の型が同じ」なのに対し、タプルは「この位置にはこの型、次の位置にはあの型」と厳格にルールを決め打ちします。
—
2. 実践:タプルを返す関数と分割代入の魔法
それでは実際に、数値を割って「商(quotient)」と「余り(remainder)」を同時に返す関数を作ってみましょう。
// 算術の結果を [商, 余り] のタプルとして返す関数
function divide(dividend: number, divisor: number): [number, number] {
const quotient = Math.floor(dividend / divisor);
const remainder = dividend % divisor;
// 返り値としてタプルリテラルを返す
return [quotient, remainder];
}
この関数の戻り値の型は `[number, number]` です。
これを呼び出すときに、TypeScriptの真骨頂である分割代入(Destructuring assignment)と型推論が美しく連携します。
// 分割代入を使って受け取る
const [q, r] = divide(10, 3);
// q と r の型はどう推論されているでしょうか?
// どちらも自動的に `number` 型になります!
console.log(`商: ${q}, 余り: ${r}`); // 商: 3, 余り: 1
ここで感動してほしいのは、変数 `q` や `r` にわざわざ `number` 型の注釈(アノテーション)を書く必要が一切ないという点です。TypeScriptのコンパイラは、`divide` が `[number, number]` を返すことを知っているため、分割代入された変数に対しても「1番目は `number`、2番目も `number` だな」と完璧に型を伝播(推論)させているのです。
—
3. 開発現場で頻出するデザイン:非同期処理のカスタムフック風パターン
実務でタプルが最も輝く瞬間の一つが、Reactの `useState` のように「値」と「それを操作する関数」をペアで返すようなパターンです。
// カウンターの状態と、それをインクリメントする関数を返すカスタムロジック
function createCounter(initialValue: number): [() => number, () => void] {
let count = initialValue;
const get = () => count;
const increment = () => {
count++;
};
return [get, increment];
}
// 使う側
const [getCount, incrementCount] = createCounter(10);
console.log(getCount()); // 10
incrementCount();
console.log(getCount()); // 11
オブジェクトのプロパティ名(キー)を考える必要がなく、自分が好きな変数名(ここでは `getCount`, `incrementCount`)を自由に命名して受け取れるのが、タプルによる多値戻り値の最大のメリットです。
—
4. 陥りやすい文法エラーと、その回避策
初学者のうちは、タプルを扱う際にいくつかハマりがちなポイントがあります。代表的なものをあらかじめ知っておきましょう。
エラーパターン1: 通常の配列として推論されてしまう
// やりがちなミス
const result = [10, “Hello”]; // 型は (string | number)[] と推論される!
TypeScriptはデフォルトで、コード上の配列リテラルを見ると「あとから要素が追加・変更されるかもしれない通常の配列」と解釈し、Union型(`string | number`)の配列にしてしまいます。これではタプルとしての厳密さが失われます。
対策:`as const`(constアセッション)を使う
「この配列はもう変更しないし、このままの型で固定して!」とTypeScriptにお願いするのが `as const` です。
// 厳密なタプル型 [10, “string”](かつ読み取り専用)として推論される
const strictResult = [10, “Hello”] as const;
エラーパターン2: 要素数を超えたアクセス
function getPoint(): [number, number] {
return [100, 200];
}
const point = getPoint();
// 3番目の要素にアクセスしようとする
console.log(point[2]); // ❌ コンパイルエラー!
// 2つの要素を持つタプルにインデックス ‘2’ でアクセスすることはできません。
TypeScriptの型システムは配列の長さを記憶しているため、存在しないインデックスへのアクセスをコンパイル段階でビシッと防いでくれます。実行時エラーを未然に防ぐ、素晴らしい安全性ですね。
—
まとめ
いかがでしたでしょうか? 今回のポイントを簡単に振り返ってみましょう。
1. タプル型は、要素の順番とそれぞれの型が固定された配列である。
2. オブジェクトを定義するほどでもない複数の戻り値に最適。
3. 分割代入を組み合わせることで、呼び出し側で自由な変数名で型安全に値を受け取れる。
4. 配列リテラルをタプルとして確実に扱いたいときは `as const` が役立つ。
ここをクリアできれば、TypeScriptの型推論のスマートさにぐっと魅力を感じるはずです。ぜひ、今日のコードをご自身のエディタで動かして、型の恩恵を体感してみてくださいね。
それでは、次のステップでも一緒にTypeScriptの深淵を楽しみましょう!