TypeScript 速習チュートリアル

TypeScript 型推論

1. TypeScript における型推論の理解

型推論(Type Inference)とは、明示的な型アノテーションを記述しなくても、TypeScript が変数の使用状況や文脈(コンテキスト)から自動的に型を決定・割り当てる機能のことです。
この強力な機能により、型安全性を維持したまま、コードの冗長性を大幅に削減できます。

1.1 主要なコンセプト

  • 型推論: 代入された値から型を自動検出します。
  • コンテキストによる型指定 (Contextual Typing): 周囲の文脈から型を推論します。
  • 最良共通型 (Best common type): 複数の値から互換性のある最適な型を見つけ出すアルゴリズムです。
  • ワイドニング(Widening)/ ナローイング(Narrowing): 使用状況に応じて型が拡張、または限定されます。
  • 発生タイミング: 変数の初期化、戻り値、デフォルトパラメータ、コールバック、リテラルなど。

コード例

// TypeScript はこれらの変数の型を自動で推論します
let name = "Alice";    // string 型と推論される
let age = 30;          // number 型と推論される
let isActive = true;   // boolean 型と推論される
let numbers = [1, 2, 3]; // number[] 型と推論される
let mixed = [1, "two", true]; // (string | number | boolean)[] 型と推論される

// 推論された型の利用
name.toUpperCase(); // name は string と推論されているため実行可能
age.toFixed(2);     // age は number と推論されているため実行可能
// name.toFixed(2); // エラー: 'toFixed' プロパティは 'string' 型に存在しません

2. 関数の戻り値の型推論

TypeScript は、関数内の return 文に基づいて戻り値の型を推論できます。

コード例

// 戻り値は string 型と推論される
function greet(name: string) {
  return `こんにちは、${name}!`;
}

// 戻り値は number 型と推論される
function add(a: number, b: number) {
  return a + b;
}

// 戻り値は string | number 型と推論される
function getValue(key: string) {
  if (key === "name") {
    return "Alice";
  } else {
    return 42;
  }
}

// 推論された戻り値型の利用
let greeting = greet("Bob"); // string 型と推論される
let sum = add(5, 3);         // number 型と推論される
let value = getValue("age"); // string | number 型と推論される

3. コンテキストによる型指定

TypeScript は、式が記述された場所の「文脈」に基づいて型を推論することもできます。

コード例

// 配列メソッドの文脈から、コールバック引数の型が推論される
const names = ["Alice", "Bob", "Charlie"];

// 引数 'name' は string 型と推論される
names.forEach(name => {
  console.log(name.toUpperCase());
});

// 引数 'name' は string 、戻り値は number 型と推論される
const nameLengths = names.map(name => {
  return name.length;
});

// nameLengths は number[] 型と推論される

// イベントハンドラのパラメータ型も推論される
document.addEventListener("click", event => {
  // 'event' は MouseEvent 型と推論される
  console.log(event.clientX, event.clientY);
});

4. オブジェクトリテラルにおける型推論

オブジェクトリテラルを使用する場合、TypeScript は各プロパティの型を詳細に推論します。

コード例

// TypeScript はこのオブジェクトの型を詳細に推論します
const user = {
  id: 1,
  name: "Alice",
  email: "[email protected]",
  active: true,
  details: {
    age: 30,
    address: {
      city: "New York",
      country: "USA"
    }
  }
};

// 推論されたプロパティへのアクセス
console.log(user.name.toUpperCase());
console.log(user.details.age.toFixed(0));
console.log(user.details.address.city.toLowerCase());

// 型エラーは適切にキャッチされる
// console.log(user.age); // エラー: プロパティ 'age' は存在しません
// console.log(user.details.name); // エラー: プロパティ 'name' は存在しません

5. アドバンスド・パターン

5.1 const アサーション

コード例

// 通常の型推論(string 型へとワイドニングされる)
let name = "Alice";  // 型: string

// const アサーション(リテラル型へとナローイングされる)
const nameConst = "Alice" as const;  // 型: "Alice"

// オブジェクトでの利用
const user = {
  id: 1,
  name: "Alice",
  roles: ["admin", "user"] as const  // 読み取り専用のタプル
} as const;

// user.name = "Bob";  // エラー: 読み取り専用プロパティのため 'name' に代入できません

5.2 型ガードと制御フロー解析

コード例

function processValue(value: string | number) {
  // このブロック内では型が string に絞り込まれる(Narrowing)
  if (typeof value === "string") {
    console.log(value.toUpperCase());
  }
  // ここでは型が number に絞り込まれる
  else {
    console.log(value.toFixed(2));
  }
}

// 識別子付き共用体(Discriminated Unions)
interface Circle { kind: "circle"; radius: number; }
interface Square { kind: "square"; size: number; }
type Shape = Circle | Square;

function area(shape: Shape) {
  // 'kind' プロパティに基づいて型が絞り込まれる
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
  }
}

6. ベストプラクティス

TypeScript の型推論を最大限に活用するためのガイドラインです。

コード例

// 1. シンプルな型は TypeScript に推論させる
let message = "こんにちは"; // Good: 明示的な型定義は不要

// 2. 関数のパラメータには明示的に型を指定する
function formatName(firstName: string, lastName: string) {
  return `${firstName} ${lastName}`;
}

// 3. 複雑な関数の場合は戻り値の型アノテーションを検討する
function processData(input: string[]): { count: number; items: string[] } {
  return {
    count: input.length,
    items: input.map(item => item.trim())
  };
}

// 4. 空の配列やオブジェクトには明示的な型指定を行う
const emptyArray: string[] = []; // 指定しない場合、any[] と推論されてしまう
const configOptions: Record<string, unknown> = {}; // 指定しない場合、{} と推論される

// 5. TypeScript が正しく推論できない場合は型アサーションを使用する
const canvas = document.getElementById("main-canvas") as HTMLCanvasElement;

7. 明示的な型定義が必要なケース

型推論は強力ですが、以下のような状況では明示的な型指定が推奨されます。

項目推奨される理由
パブリック API のコントラクトライブラリコードの引数や戻り値を固定し、意図しない変更を防ぐため。
複雑な型推論された型が広すぎたり、複雑すぎて可読性が下がる場合。
ドキュメント化コードそのものをドキュメントとして分かりやすくするため。
型安全性の強制特定の制約(Constraint)を厳密に守らせたい場合。
空のコレクション後で値が追加される空の配列やオブジェクト。

7.1 パフォーマンスと可読性のための工夫

コード例

// Good: 複雑な戻り値には明示的な型を指定
function processData(input: string[]): { results: string[]; count: number } {
  return {
    results: input.map(processItem),
    count: input.length
  };
}

// Good: 空の配列には明示的な型を指定
const items: Array<{ id: number; name: string }> = [];

// Good: 設定用オブジェクトには明示的な型を指定
const config: {
  apiUrl: string;
  retries: number;
  timeout: number;
} = {
  apiUrl: "https://api.example.com",
  retries: 3,
  timeout: 5000
};