デザインについての学習メモブログ

TypeScript入門 #3:関数と型、ジェネリクス入門

記事内に広告が含まれています。

TypeScript入門 #3:関数と型、ジェネリクス入門

第1回、第2回では基本的な型を学びました。

TypeScript入門第3回は、基本的な型の理解を超えて、より高度で柔軟なコードを書けるようになる 関数の型定義とジェネリクス をわかりやすく解説します。

本記事は、 関数の基本から、戻り値の型、オプショナル・デフォルト・Rest パラメータ、そして実務でも頻出の ジェネリクス(Generics) の考え方まで丁寧にステップアップ形式で学べる内容です。

TypeScript の力を最大限に引き出し、型安全で再利用性の高い関数設計をマスターしましょう。

関数の基本的な型定義

基本形

TypeScript
// 関数宣言
function add(a: number, b: number): number {
  return a + b;
}

// 関数式
const subtract = function(a: number, b: number): number {
  return a - b;
};

// アロー関数
const multiply = (a: number, b: number): number => {
  return a * b;
};

// アロー関数(省略形)
const divide = (a: number, b: number): number => a / b;

戻り値の型(neverについても)

TypeScript
// 明示的に戻り値の型を指定
function greet(name: string): string {
  return `Hello, ${name}!`;
}

// 型推論に任せる(戻り値の型は自動的にstringと推論される)
function greet2(name: string) {
  return `Hello, ${name}!`;
}

// 戻り値がない場合はvoid
function logMessage(message: string): void {
  console.log(message);
  // returnなし、またはreturn;のみ
}

// neverは「決して戻らない」関数
function throwError(message: string): never {
  throw new Error(message);
  // この関数は例外をスローするので絶対に戻らない
}

function infiniteLoop(): never {
  while (true) {
    // 無限ループで戻らない
  }
}

never は「このコードパスには絶対に値が存在しない」ことをTypeScriptに伝える型
👉 「never = 到達不能」

以下のような時に使われます。

  • エラー処理
  • 無限ループ
  • union型の網羅チェック

オプショナルパラメータ

TypeScriptの オプショナルパラメータ は、「渡してもいいし、渡さなくてもいい引数」 を表します。

👉 「? を付けると型に undefined が追加される」

TypeScript
// ?を付けるとオプショナル(省略可能)
function greet(name: string, age?: number): string {
  if (age !== undefined) {
    return `Hello, ${name}! You are ${age} years old.`;
  }
  return `Hello, ${name}!`;
}

greet("太郎");           // OK
greet("太郎", 25);       // OK

// 注意:オプショナルパラメータは必須パラメータの後に配置
function invalid(age?: number, name: string) {}  // エラー!

デフォルトパラメータ

TypeScript
// デフォルト値を設定
function greet(name: string, greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}

greet("太郎");                    // "Hello, 太郎!"
greet("太郎", "こんにちは");       // "こんにちは, 太郎!"

// デフォルトパラメータは自動的にオプショナルになる
function createUser(name: string, role: string = "user") {
  return { name, role };
}


デフォルト値を持つ引数も、同じく「省略可能」 として扱われます。

オプショナルとの使い分けの目安

  • 値がなくても処理できる → オプショナル
  • 省略時の挙動を決めたい → デフォルトパラメータ

Restパラメータ

メソッドの宣言で引数の前に”…”とすることで可変長引数を受け取ります。
…numbers に渡された引数が 配列 になります。

サンプルコードでは、メソッドの実行時に引数の個数が違っても問題なく受け取る事ができています。

TypeScript
// 任意の数の引数を配列として受け取る
function sum(...numbers: number[]): number {
  return numbers.reduce((total, n) => total + n, 0);
}

sum(1, 2, 3);           // 6
sum(1, 2, 3, 4, 5);     // 15

// 通常のパラメータと組み合わせ
function introduce(greeting: string, ...names: string[]): string {
  return `${greeting}, ${names.join(" and ")}!`;
}

introduce("Hello", "太郎", "花子", "次郎");
// "Hello, 太郎 and 花子 and 次郎!"

オプショナルパラメータとの違い

TypeScript
function example(a?: number, …rest: number[]) {}
  • a → 0 or 1個
  • rest → 0個以上

タプルと組み合わせる(TypeScriptらしい使い方)

TypeScript
function log(…args: [string, number]) {
  const [message, count] = args;
}

log("hello", 3); // OK
log("hello");    // ❌ エラー

👉 引数の数と型を厳密に制御できる

ジェネリクスと組み合わせる例

TypeScript
function first(…args: T[]): T | undefined {
  return args[0];
}

よくある混同:スプレッド構文との違い

種類使う場所意味
Rest関数定義引数をまとめる
Spread関数呼び出し配列を展開
TypeScript
// Spread
const nums = [1, 2, 3];
sum(…nums);

// Rest
function sum(…nums: number[]) {}

一言でまとめると

Restパラメータ = 「残りの引数を配列で受け取る」

  • 可変長引数を扱える
  • TypeScriptでは型安全に書ける
  • タプルと組み合わせると強力

関数の型

関数自体を変数に代入したり、引数として渡したりする場合の型定義です。

関数型の定義

TypeScript
// 方法1:アロー関数形式
let myAdd: (a: number, b: number) => number;

myAdd = function(x: number, y: number): number {
  return x + y;
};



// 方法2:型エイリアスを使用
type MathOperation = (a: number, b: number) => number;

let add: MathOperation = (x, y) => x + y;
let subtract: MathOperation = (x, y) => x - y;
let multiply: MathOperation = (x, y) => x * y;

// 関数を引数に取る関数
function calculate(a: number, b: number, operation: MathOperation): number {
  return operation(a, b);
}
calculate(10, 5, add);
  // 15
calculate(10, 5, subtract);
  // 5
calculate(10, 5, multiply);
  // 50

コールバック関数の型定義

TypeScript
// コールバック関数を受け取る
function fetchData(callback: (data: string) => void): void {
  const data = "取得したデータ";
  callback(data);
}

fetchData((data) => {
  console.log(data);
});



// エラーハンドリング付き
type Callback = (error: Error | null, data: string | null) => void;

function fetchDataWithError(callback: Callback): void {
  try {
    const data = "取得したデータ";
    callback(null, data);
  } catch (error) {
    callback(error as Error, null);
  }
}

// 使用例
fetchDataWithError((error, data) => {
  if (error) {
    console.error("エラー:", error);
  } else {
    console.log("データ:", data);
  }
});

関数のオーバーロード

同じ関数名で異なる引数の型を受け入れたい場合に使います。

TypeScript
// オーバーロードシグネチャ
function format(value: string): string;
function format(value: number): string;
function format(value: boolean): string;

// 実装シグネチャ
function format(value: string | number | boolean): string {
  if (typeof value === "string") {
    return `"${value}"`;
  } else if (typeof value === "number") {
    return value.toFixed(2);
  } else {
    return value ? "true" : "false";
  }
}

format("hello");    // "hello"
format(123.456);    // "123.46"
format(true);       // "true"


// より実践的な例
function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: "span"): HTMLSpanElement;
function createElement(tag: "button"): HTMLButtonElement;
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

const div = createElement("div");      // HTMLDivElement型
const span = createElement("span");    // HTMLSpanElement型

「より実践的な例」では、引数によって戻り値の型を厳密にできる。

ジェネリクス入門

ジェネリクス(Generics) は、「型をあとから決められる仕組み」 です。

ジェネリクスは、型を「引数」として受け取ることで、柔軟で再利用可能なコードを書く強力な機能です。

ジェネリクスが必要な理由

TypeScript
// ジェネリクスを使わない場合
function getFirstString(arr: string[]): string {
  return arr[0];
}

function getFirstNumber(arr: number[]): number {
  return arr[0];
}

// 型ごとに関数を作る必要がある...面倒!

// anyを使うと型安全性が失われる
function getFirstAny(arr: any[]): any {
  return arr[0];
}

const result = getFirstAny([1, 2, 3]);  // any型になってしまう

ジェネリクスの基本

TypeScript
// <T>は型パラメータ(慣例的にTを使う)
function getFirst<T>(arr: T[]): T {
  return arr[0];
}

// 使用時に型を指定
const firstNumber = getFirst<number>([1, 2, 3]);     // number型
const firstName = getFirst<string>(["a", "b", "c"]); // string型

// 型推論も効く
const first = getFirst([1, 2, 3]);  // number型と推論される

一言でいうと👇

ジェネリクス = 型の変数

複数の型パラメータ

TypeScript
// 2つの型パラメータを持つ関数
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const p1 = pair<string, number>("age", 25);      // [string, number]
const p2 = pair("name", "太郎");                  // 型推論が効く
const p3 = pair<boolean, string[]>(true, ["a"]); // [boolean, string[]]

// 実用例:キーと値のペアを作る
function createEntry<K, V>(key: K, value: V): { key: K; value: V } {
  return { key, value };
}

const entry1 = createEntry("name", "太郎");
const entry2 = createEntry(1, { id: 100, name: "商品A" });

ジェネリック型エイリアス

TypeScript
// レスポンスの型をジェネリックで定義
type ApiResponse<T> = {
  success: boolean;
  data: T;
  message: string;
};

// 具体的な型で使用
type UserResponse = ApiResponse<{ id: number; name: string }>;
type ProductResponse = ApiResponse<{ id: number; name: string; price: number }>;

// 使用例
const userResponse: UserResponse = {
  success: true,
  data: { id: 1, name: "太郎" },
  message: "ユーザー取得成功"
};

// 配列のレスポンス
type ListResponse<T> = {
  success: boolean;
  data: T[];
  total: number;
};

const productsResponse: ListResponse<{ id: number; name: string }> = {
  success: true,
  data: [
    { id: 1, name: "商品A" },
    { id: 2, name: "商品B" }
  ],
  total: 2
};

ジェネリックインターフェース

TypeScript
// ジェネリックなインターフェース
interface Box<T> {
  value: T;
  getValue: () => T;
  setValue: (value: T) => void;
}

// 具体的な型で使用
const numberBox: Box<number> = {
  value: 42,
  getValue() {
    return this.value;
  },
  setValue(value: number) {
    this.value = value;
  }
};

const stringBox: Box<string> = {
  value: "hello",
  getValue() {
    return this.value;
  },
  setValue(value: string) {
    this.value = value;
  }
};

制約付きジェネリクス

型パラメータに制約を加えることができます。

TypeScript
// extendsで制約を追加
interface HasLength {
  length: number;
}

// TはHasLengthを持つ型でなければならない
function logLength<T extends HasLength>(item: T): void {
  console.log(item.length);
}

logLength("hello");        // OK(stringはlengthを持つ)
logLength([1, 2, 3]);      // OK(配列はlengthを持つ)
logLength({ length: 10 }); // OK
// logLength(123);         // エラー!numberはlengthを持たない

// より実践的な例
interface Identifiable {
  id: number;
}

function findById<T extends Identifiable>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id);
}

const users = [
  { id: 1, name: "太郎", email: "taro@example.com" },
  { id: 2, name: "花子", email: "hanako@example.com" }
];

const user = findById(users, 1);  // { id: number; name: string; email: string } | undefined

keyofとジェネリクス

オブジェクトのキーを型安全に扱えます。

TypeScript
// keyofでオブジェクトのキーの型を取得
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person = {
  name: "太郎",
  age: 25,
  email: "taro@example.com"
};

const name = getProperty(person, "name");    // string型
const age = getProperty(person, "age");      // number型
// const invalid = getProperty(person, "invalid");  // エラー!

// 実用例:オブジェクトの値を更新する関数
function updateProperty<T, K extends keyof T>(
  obj: T,
  key: K,
  value: T[K]
): void {
  obj[key] = value;
}

updateProperty(person, "age", 26);        // OK
// updateProperty(person, "age", "26");   // エラー!型が合わない

実践例:汎用的なデータフェッチ関数

学んだ知識を使って、実践的な関数を作ってみましょう。

TypeScript
// APIレスポンスの型
type ApiResponse<T> = {
  success: boolean;
  data: T;
  error?: string;
};

// フェッチ関数(ジェネリック)
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  try {
    const response = await fetch(url);
    const data: T = await response.json();
    return {
      success: true,
      data
    };
  } catch (error) {
    return {
      success: false,
      data: {} as T,
      error: error instanceof Error ? error.message : "Unknown error"
    };
  }
}

// ユーザーの型定義
type User = {
  id: number;
  name: string;
  email: string;
};

// 商品の型定義
type Product = {
  id: number;
  name: string;
  price: number;
};

// 使用例
async function loadUser() {
  const response = await fetchData<User>("/api/users/1");
  
  if (response.success) {
    console.log(response.data.name);  // string型
    console.log(response.data.email); // string型
  } else {
    console.error(response.error);
  }
}

async function loadProducts() {
  const response = await fetchData<Product[]>("/api/products");
  
  if (response.success) {
    response.data.forEach(product => {
      console.log(product.name, product.price);
    });
  }
}

実践例:汎用的なリストフィルター

TypeScript
// フィルター条件の型
type FilterCondition<T> = (item: T) => boolean;

// 汎用的なフィルター関数
function filterList<T>(items: T[], conditions: FilterCondition<T>[]): T[] {
  return items.filter(item => 
    conditions.every(condition => condition(item))
  );
}

// ユーザーリストをフィルター
type User = {
  id: number;
  name: string;
  age: number;
  isActive: boolean;
};

const users: User[] = [
  { id: 1, name: "太郎", age: 25, isActive: true },
  { id: 2, name: "花子", age: 30, isActive: false },
  { id: 3, name: "次郎", age: 35, isActive: true }
];

// フィルター条件を定義
const isAdult: FilterCondition<User> = (user) => user.age >= 20;
const isActive: FilterCondition<User> = (user) => user.isActive;

// 20歳以上でアクティブなユーザーを取得
const activeAdults = filterList(users, [isAdult, isActive]);
console.log(activeAdults);
// [{ id: 1, name: "太郎", age: 25, isActive: true },
//  { id: 3, name: "次郎", age: 35, isActive: true }]

実践例:型安全なイベントエミッター

TypeScript
// イベント名と引数の型のマッピング
type EventMap = {
  login: { userId: number; timestamp: Date };
  logout: { userId: number };
  purchase: { productId: number; amount: number };
};

// イベントハンドラーの型
type EventHandler<T> = (data: T) => void;

// 型安全なイベントエミッター
class TypedEventEmitter<T extends Record<string, any>> {
  private listeners: {
    [K in keyof T]?: EventHandler<T[K]>[];
  } = {};

  // イベントリスナーを登録
  on<K extends keyof T>(event: K, handler: EventHandler<T[K]>): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(handler);
  }

  // イベントを発火
  emit<K extends keyof T>(event: K, data: T[K]): void {
    const handlers = this.listeners[event];
    if (handlers) {
      handlers.forEach(handler => handler(data));
    }
  }
}

// 使用例
const emitter = new TypedEventEmitter<EventMap>();

// 型安全にリスナーを登録
emitter.on("login", (data) => {
  console.log(`ユーザー ${data.userId} がログインしました`);
  console.log(`時刻: ${data.timestamp}`);
});

emitter.on("purchase", (data) => {
  console.log(`商品 ${data.productId}${data.amount} 円で購入`);
});

// 型安全にイベントを発火
emitter.emit("login", { userId: 1, timestamp: new Date() });
emitter.emit("purchase", { productId: 100, amount: 5000 });

// エラーになる例
// emitter.emit("login", { userId: "1" });  // エラー!userIdはnumber
// emitter.emit("invalid", {});             // エラー!存在しないイベント

補足

TypedEventEmittet.listenersに格納されるデーターのイメージは、

listeners
 ├─ login    → [ handler1, handler2, handler3 ]
 ├─ logout   → [ handlerA ]
 └─ purchase → [ handlerX, handlerY ]

一つのイベントに複数のハンドラーが格納できるようにしているのは

例えば、「ユーザーがログインした」という1つの出来事に対して、やりたいことは複数あります。

  • 画面に「ログイン成功」を表示したい
  • ログイン履歴を保存したい
  • アクセス解析に送信したい
  • 管理者向け通知を送りたい

👉 全部「ログイン」という1イベントに反応している

ジェネリクスのベストプラクティス

✅ 良い例

TypeScript
// 1. 明確な命名(Tだけでなく意味のある名前も使える)
function createPair<TKey, TValue>(key: TKey, value: TValue) {
  return { key, value };
}

// 2. 適切な制約
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

// 3. デフォルト型パラメータ
type ApiResponse<T = unknown> = {
  data: T;
  status: number;
};

unknown 型は、「型がまだ分からない値」を安全に扱うための型です。

unknown = 安全版 any

型を確認すれば使えるようになります。(anyはTypeScriptの型チェックが無意味になります。)

❌ 避けるべき例

TypeScript
// 1. 不必要なジェネリクス
function bad<T>(value: T): T {
  return value;  // 単に値を返すだけならジェネリクス不要
}

// 2. 制約のないany的な使い方
function veryBad<T>(value: T): any {
  return value;  // 戻り値がanyになって型安全性が失われる
}

// 3. 過度に複雑なジェネリクス
type TooComplex<T, U, V, W, X, Y, Z> = ...;  // 読みにくい

よくある質問

Q1: ジェネリクスとanyの違いは?

TypeScript
// anyは型情報が失われる
function identityAny(value: any): any {
  return value;
}

const result1 = identityAny(123);
// result1はany型なので、どんなメソッドでも呼べてしまう(危険)
result1.toUpperCase();  // エラーにならないが実行時に失敗する

// ジェネリクスは型情報が保持される
function identity<T>(value: T): T {
  return value;
}

const result2 = identity(123);
// result2はnumber型として推論される
// result2.toUpperCase();  // コンパイルエラー!事前に問題を見つけることができる。

Q2: いつジェネリクスを使うべき?

ジェネリクスを使うべき場合:

  • 複数の型で同じロジックを使いたい
  • 型安全性を保ちながら柔軟性が必要
  • 配列操作、データ構造、ユーティリティ関数など

ジェネリクスが不要な場合:

  • 特定の型でしか使わない関数
  • シンプルな値の変換
  • 型が明確で変わらない場合

まとめ

今回は、関数の型定義とジェネリクスについて学びました。

学んだこと

  • 関数の基本的な型定義
  • オプショナルパラメータ、デフォルトパラメータ、Restパラメータ
  • 関数型と関数のオーバーロード
  • ジェネリクスの基本概念
  • 制約付きジェネリクス
  • keyofとジェネリクスの組み合わせ
  • 実践的なジェネリクスの使用例

次回予告 第4回では、クラスとオブジェクト指向、より高度な型操作について学びます。

TypeScript入門シリーズ

第1回: 環境構築から全体像まで
第2回: 型の基礎を深く理解する
第3回: 関数と型、ジェネリクス入門
第4回: インターフェースとクラス
第5回: 実践編 – 実際のプロジェクトでの活用