TypeScriptのタプル型とany/unknown/never型を理解しよう

先生、TypeScriptのタプル型って何ですか?普通の配列と何が違うんでしょうか?

いい質問だね!まず普通の配列から説明しよう。

// 普通の配列:どれも同じ型、個数は自由
const numbers: number[] = [1, 2, 3, 4, 5]; // 何個でもOK
const fruits: string[] = ["りんご", "みかん"]; // 何個でもOK

普通の配列は同じ型の要素を何個でも入れられる。でもタプル型は違うんだ。

// タプル型:型と順番と個数が決まっている
const studentInfo: [string, number, boolean] = ["田中太郎", 17, true];
//                  ↑名前    ↑年齢  ↑部活に所属しているか

// この順番と個数を守らないとエラーになる
// const wrongInfo: [string, number, boolean] = ["佐藤", 16]; // エラー!個数が足りない
// const alsoWrong: [string, number, boolean] = [18, "鈴木", false]; // エラー!順番が違う

なるほど!タプルは『この位置にはこの型』って決まってるんですね。でも、これってどんな時に使うんですか?

タプル型の使い道

実は身近なところで使われているよ。例えば、ゲームのキャラクターのステータスを表現する時とか。

// RPGキャラクターのステータス [HP, MP, レベル, 名前, 職業]
const hero: [number, number, number, string, string] = [100, 50, 5, "勇者アキラ", "戦士"];
const mage: [number, number, number, string, string] = [60, 120, 7, "魔法使いユキ", "魔法使い"];

// 順番が決まっているので、取り出すときも分かりやすい
const [hp, mp, level, name, job] = hero;
console.log(`${name}のHP: ${hp}, MP: ${mp}, レベル: ${level}, 職業: ${job}`);
// 出力: 勇者アキラのHP: 100, MP: 50, レベル: 5, 職業: 戦士

関数の戻り値で複数の値を返したい時にもよく使うよ。

// 計算結果と成功したかどうかを同時に返す関数
function divide(a: number, b: number): [number, boolean] {
  if (b === 0) {
    return [0, false]; // 割り算失敗
  }
  return [a / b, true]; // 割り算成功
}

// 使う側
const [result, success] = divide(10, 2);
if (success) {
  console.log(`計算結果: ${result}`); // 計算結果: 5
} else {
  console.log("計算に失敗しました");
}

おお!これは便利そうですね。でも、要素数が決まってない場合はどうするんですか?

そんな時はレストパラメータが使えるよ。

// 最初の要素は必ずnumber、その後は任意の数のstring
const gameData: [number, ...string[]] = [1, "スライム", "ゴブリン", "オーク", "ドラゴン"];
//                ↑ゲームID  ↑出現する敵の名前(何体でもOK)

// 分割代入で取り出せる
const [gameId, ...enemies] = gameData;
console.log(`ゲームID: ${gameId}`); // ゲームID: 1
console.log(`敵の数: ${enemies.length}`); // 敵の数: 4
console.log(`敵一覧: ${enemies.join(", ")}`); // 敵一覧: スライム, ゴブリン, オーク, ドラゴン

any型 – 「何でもあり」の危険な型

タプルは分かりました!次は any型について教えてください。

any型は… 正直あまり使わない方がいい型なんだ。でも理解は必要だね。

// any型:何でも入る魔法の箱(でも危険)
let magicBox: any = 42;        // 数字OK
magicBox = "Hello";            // 文字列OK
magicBox = true;               // 真偽値OK
magicBox = { name: "太郎" };   // オブジェクトOK
magicBox = [1, 2, 3];          // 配列OK

// でも、これが問題...
console.log(magicBox.name);           // OK(オブジェクトの時)
console.log(magicBox.toUpperCase());  // 実行時エラー!(数字や真偽値には.toUpperCase()がない)

any型の一番の問題は、実行してみるまでエラーが分からないことなんだ。

// JSONデータを解析する例
const jsonData = '{"id": 123, "username": "yamada"}';
const user: any = JSON.parse(jsonData); // JSON.parseの戻り値はany型

// コンパイル時はエラーにならない(anyだから何でもOK)
console.log(user.id);              // 123(正常)
console.log(user.username);        // yamada(正常)
console.log(user.profile.age);     // 実行時エラー!profileプロパティは存在しない

うわあ、これは怖いですね…。TypeScriptを使ってる意味がなくなっちゃう。

unknown型 – 安全な「何でもあり」

そこで登場するのがunknown型だ!any型の安全版だよ。

// unknown型:何でも入るけど、安全にチェックしてから使う
let safeBox: unknown = "Hello World";

// そのまま使おうとするとエラーになる
// console.log(safeBox.length);        // エラー!unknownには.lengthがない
// console.log(safeBox.toUpperCase()); // エラー!unknownには.toUpperCase()がない

// 型をチェックしてから使う(型ガード)
if (typeof safeBox === "string") {
  // この中では safeBox は string型として扱われる
  console.log(safeBox.length);        // OK!
  console.log(safeBox.toUpperCase()); // OK!
}

JSONデータの例も、unknown型を使うと安全になるよ。

// より安全なJSONデータの処理
const jsonData2 = '{"id": 456, "username": "tanaka"}';
const userData: unknown = JSON.parse(jsonData2);

// 型をチェックしてから使用
if (typeof userData === "object" && userData !== null) {
  // オブジェクトであることを確認した
  const user = userData as { id?: number; username?: string }; // 型アサーション
  
  if (user.id && user.username) {
    console.log(`ユーザーID: ${user.id}, 名前: ${user.username}`);
  } else {
    console.log("必要なデータが不足しています");
  }
} else {
  console.log("JSONデータの形式が正しくありません");
}

なるほど!unknown型は『何でも入るけど、きちんと確認してから使ってね』ってことですね。

never型 – 「何も入らない」不思議な型

最後はnever型。これは『絶対に値が入らない型』なんだ。

え?何も入らないなら使い道がないんじゃないですか?

実は、プログラムの安全性を高める重要な役割があるんだよ。例を見てみよう。

// ゲームキャラクターの種族を表す型
type CharacterRace = "エルフ" | "ドワーフ" | "人間";

function getCharacterBonus(race: CharacterRace): string {
  switch (race) {
    case "エルフ":
      return "魔法攻撃力+10";
    case "ドワーフ":
      return "物理防御力+15";
    case "人間":
      return "経験値獲得量+20%";
    default:
      // ここに到達することは「絶対にない」はず
      const check: never = race; // すべてのケースを処理済みならraceはnever型になる
      throw new Error(`未対応の種族: ${race}`);
  }
}

console.log(getCharacterBonus("エルフ")); // 魔法攻撃力+10

もし新しい種族を追加し忘れたらどうなるか見てみよう。

// 新しい種族を追加
type CharacterRace2 = "エルフ" | "ドワーフ" | "人間" | "オーク";

function getCharacterBonus2(race: CharacterRace2): string {
  switch (race) {
    case "エルフ":
      return "魔法攻撃力+10";
    case "ドワーフ":
      return "物理防御力+15";
    case "人間":
      return "経験値獲得量+20%";
    // case "オーク": を書き忘れた!
    default:
      // ここでエラーが発生!"オーク"はnever型に代入できない
      const check: never = race; // ← ここでコンパイルエラー
      throw new Error(`未対応の種族: ${race}`);
  }
}

おお!コンパイル時点で『オークのケースを書き忘れてるよ』って教えてくれるんですね!

そう!never型を使うことで、すべてのケースを処理したかどうかをTypeScriptがチェックしてくれるんだ。これを『網羅性チェック』というよ。

まとめ

今日学んだことをまとめてみよう。

タプル型

  • 配列の各要素の型、順番、個数が決まっている
  • 関数の戻り値や、決まった形のデータを表現するのに便利
  • 分割代入と組み合わせて使うことが多い

any型

  • 何でも代入できるが、型チェックが働かない
  • 実行時エラーの原因になりやすいので、できるだけ避ける
  • レガシーコードやライブラリとの互換性のために時々使う

unknown型

  • anyの安全版。何でも代入できるが、使用前に型チェックが必要
  • 型安全性を保ちながら柔軟な処理ができる
  • JSONデータの処理などで活用

never型

  • 何も代入できない型
  • 網羅性チェックで、すべてのケースを処理したかを確認できる
  • エラーを投げる関数や、無限ループの関数の戻り値型としても使用

最初は難しそうに見えたけど、それぞれちゃんと使い道があるんですね!特にnever型の網羅性チェックは、バグを防ぐのに役立ちそうです。

その通り!型システムを理解して使いこなせるようになると、より安全で保守しやすいコードが書けるようになるよ。今日はお疲れさまでした!