はじめに

前回の記事では、TypeScriptにおける関数の型注釈、オプショナル引数、デフォルト引数、残余引数、オーバーロードについて解説しました。本記事では、その知識を発展させて、コールバック関数と高階関数の型定義について詳しく解説します。

JavaScriptでは、関数を引数として受け取ったり、関数を返したりする「高階関数」が頻繁に使われます。配列メソッドのmap、filter、reduceはその代表例です。TypeScriptでこれらを型安全に扱うには、コールバック関数の型を正しく定義する必要があります。

この記事を読み終えると、以下のことができるようになります。

  • コールバック関数の型を適切に定義できる
  • 高階関数の型パラメータと型推論を理解できる
  • 関数を返す関数(カリー化など)の型を定義できる
  • thisパラメータに型を付けて安全なメソッド呼び出しを実現できる
  • 実践的なユースケースでコールバック型を活用できる

実行環境・前提条件

前提知識

  • TypeScriptの基本的な型注釈(基本型入門を参照)
  • 関数の型定義(関数型の解説を参照)
  • JavaScriptの配列メソッド(map、filter、reduce)の基本

動作確認環境

ツール バージョン
Node.js 20.x以上
TypeScript 5.7以上
VS Code 最新版

本記事のサンプルコードは、TypeScript Playgroundで動作確認できます。ローカル環境で実行する場合は、開発環境構築ガイドを参照してください。

期待される結果

本記事のコードを実行すると、以下の動作を確認できます。

  • コールバック関数の引数と戻り値の型が正しくチェックされる
  • 高階関数を呼び出した際に、コールバックの引数型が自動推論される
  • 型に合わない関数を渡すとコンパイルエラーが発生する
  • thisの型が正しく推論され、不正なアクセスがエラーになる

TypeScriptのコールバック型の全体像

コールバック関数と高階関数の型定義には、さまざまなパターンがあります。まずは全体像を把握しましょう。

graph TB
    A[コールバックと高階関数の型] --> B[コールバック型の定義]
    A --> C[高階関数の型推論]
    A --> D[関数を返す関数]
    A --> E[thisの型付け]
    B --> F[型エイリアスによる定義]
    B --> G[インライン型定義]
    B --> H[ジェネリックコールバック]
    C --> I[map・filter・reduce]
    C --> J[カスタム高階関数]
    D --> K[カリー化]
    D --> L[ファクトリ関数]
    E --> M[thisParameterType]
    E --> N[OmitThisParameter]

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

基本的なコールバック型

コールバック関数とは、他の関数に引数として渡される関数のことです。TypeScriptでは、コールバック関数の型を明示的に定義できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// コールバック関数の型を直接記述
function executeCallback(callback: (message: string) => void): void {
  callback("Hello from callback!");
}

// 使用例
executeCallback((msg) => {
  console.log(msg); // Hello from callback!
});

// 型が一致しない場合はコンパイルエラー
// executeCallback((num: number) => {
//   console.log(num);
// });
// Error: Type 'number' is not assignable to type 'string'

上記の例では、callbackパラメータの型として(message: string) => voidを指定しています。これは「string型の引数を1つ受け取り、何も返さない関数」を意味します。

型エイリアスによるコールバック型の定義

コールバック関数の型が複雑になる場合や、複数箇所で再利用する場合は、型エイリアスを使って定義すると可読性が向上します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// コールバック型を型エイリアスで定義
type MessageCallback = (message: string) => void;
type ErrorCallback = (error: Error) => void;
type ResultCallback<T> = (error: Error | null, result: T | null) => void;

// 型エイリアスを使った関数定義
function processWithCallback(data: string, onSuccess: MessageCallback, onError: ErrorCallback): void {
  try {
    const processed = data.toUpperCase();
    onSuccess(processed);
  } catch (e) {
    onError(e instanceof Error ? e : new Error(String(e)));
  }
}

// 使用例
processWithCallback(
  "hello",
  (msg) => console.log("Success:", msg),  // Success: HELLO
  (err) => console.error("Error:", err.message)
);

型エイリアスを使うことで、以下のメリットがあります。

  • コードの可読性が向上する
  • 同じ型を複数箇所で再利用できる
  • 型の変更が一箇所で済む

ジェネリックコールバック型

さまざまな型のデータを扱うコールバックには、ジェネリクスを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ジェネリックなコールバック型
type Callback<T> = (error: Error | null, result: T) => void;
type AsyncCallback<T, E = Error> = (error: E | null, result: T | null) => void;

// ジェネリックコールバックを使う関数
function fetchData<T>(url: string, callback: Callback<T>): void {
  // 非同期処理のシミュレーション
  setTimeout(() => {
    // 成功時
    const mockData = { id: 1, name: "Sample" } as T;
    callback(null, mockData);
  }, 100);
}

// 使用例 - 型パラメータを明示
interface User {
  id: number;
  name: string;
}

fetchData<User>("https://api.example.com/user/1", (error, user) => {
  if (error) {
    console.error(error);
    return;
  }
  // userはUser型として推論される
  console.log(user.id, user.name);
});

オプショナルなコールバック

コールバックが省略可能な場合は、オプショナルパラメータとして定義します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type CompletionCallback = () => void;

function processTask(task: string, onComplete?: CompletionCallback): void {
  console.log(`Processing: ${task}`);
  
  // コールバックが渡された場合のみ実行
  if (onComplete) {
    onComplete();
  }
  // または optional chaining を使用
  // onComplete?.();
}

// コールバックあり
processTask("Task 1", () => {
  console.log("Task 1 completed!");
});

// コールバックなし
processTask("Task 2");

高階関数の型推論

高階関数とは

高階関数とは、関数を引数として受け取るか、関数を戻り値として返す関数のことです。TypeScriptの配列メソッドであるmap、filter、reduceは高階関数の代表例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const numbers = [1, 2, 3, 4, 5];

// map: 各要素を変換
const doubled = numbers.map((n) => n * 2);
// doubled: number[] と推論される

// filter: 条件に合う要素を抽出
const evens = numbers.filter((n) => n % 2 === 0);
// evens: number[] と推論される

// reduce: 要素を集約
const sum = numbers.reduce((acc, n) => acc + n, 0);
// sum: number と推論される

TypeScriptは、これらの高階関数に渡されるコールバック関数の引数型を自動的に推論します。

map関数の型推論の仕組み

TypeScriptのArray.prototype.mapの型定義を見てみましょう。

1
2
3
4
// Array.prototype.map の型定義(簡略版)
interface Array<T> {
  map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: unknown): U[];
}

この型定義により、以下の型推論が行われます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface User {
  id: number;
  name: string;
  age: number;
}

const users: User[] = [
  { id: 1, name: "Alice", age: 30 },
  { id: 2, name: "Bob", age: 25 },
  { id: 3, name: "Charlie", age: 35 },
];

// コールバックの引数userはUser型として推論される
const names = users.map((user) => user.name);
// names: string[] と推論される

// オブジェクトを返す場合
const userSummaries = users.map((user) => ({
  id: user.id,
  displayName: `${user.name} (${user.age})`,
}));
// userSummaries: { id: number; displayName: string }[] と推論される

filter関数の型推論と型ガード

filter関数では、型ガードを使うことでより正確な型推論が可能です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const mixedArray: (string | number | null)[] = [1, "hello", null, 2, "world", null];

// 通常のfilter - 型は変わらない
const nonNull = mixedArray.filter((item) => item !== null);
// nonNull: (string | number | null)[] のまま

// 型ガードを使ったfilter - 型が絞り込まれる
const nonNullTyped = mixedArray.filter((item): item is string | number => item !== null);
// nonNullTyped: (string | number)[] に絞り込まれる

// 特定の型だけを抽出
const stringsOnly = mixedArray.filter((item): item is string => typeof item === "string");
// stringsOnly: string[] に絞り込まれる

console.log(stringsOnly); // ["hello", "world"]

型ガード関数(item is T形式)を使うことで、filterの戻り値の型が正しく絞り込まれます。

reduce関数の型推論

reduce関数は初期値の型によって累積値の型が決まります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
interface Product {
  name: string;
  price: number;
  quantity: number;
}

const products: Product[] = [
  { name: "Apple", price: 100, quantity: 3 },
  { name: "Banana", price: 80, quantity: 5 },
  { name: "Orange", price: 120, quantity: 2 },
];

// 数値を返す場合
const totalPrice = products.reduce((sum, product) => sum + product.price * product.quantity, 0);
// totalPrice: number と推論される

// オブジェクトを返す場合
const productMap = products.reduce(
  (map, product) => {
    map[product.name] = product.price;
    return map;
  },
  {} as Record<string, number>
);
// productMap: Record<string, number> と推論される

// 配列を返す場合
const expandedProducts = products.reduce<string[]>((arr, product) => {
  for (let i = 0; i < product.quantity; i++) {
    arr.push(product.name);
  }
  return arr;
}, []);
// expandedProducts: string[]

初期値の型が推論できない場合は、型パラメータを明示するか、型アサーションを使用します。

カスタム高階関数の型定義

独自の高階関数を作成する場合、ジェネリクスを活用して型安全な関数を定義できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 配列の各要素に対して処理を実行する高階関数
function forEach<T>(array: T[], callback: (item: T, index: number) => void): void {
  for (let i = 0; i < array.length; i++) {
    callback(array[i], i);
  }
}

// 条件に合う最初の要素を返す高階関数
function find<T>(array: T[], predicate: (item: T) => boolean): T | undefined {
  for (const item of array) {
    if (predicate(item)) {
      return item;
    }
  }
  return undefined;
}

// 使用例
const numbers = [1, 2, 3, 4, 5];

forEach(numbers, (num, idx) => {
  console.log(`Index ${idx}: ${num}`);
});

const found = find(numbers, (n) => n > 3);
console.log(found); // 4

関数を返す関数の型定義

基本的な関数ファクトリ

関数を返す関数(関数ファクトリ)の型定義について解説します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 乗算関数を生成するファクトリ
function createMultiplier(factor: number): (value: number) => number {
  return (value) => value * factor;
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

// 戻り値の型を型エイリアスで定義
type Multiplier = (value: number) => number;

function createMultiplierV2(factor: number): Multiplier {
  return (value) => value * factor;
}

カリー化関数の型定義

カリー化とは、複数の引数を取る関数を、引数を1つずつ取る関数の連鎖に変換する技法です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 通常の関数
function add(a: number, b: number): number {
  return a + b;
}

// カリー化された関数
function curriedAdd(a: number): (b: number) => number {
  return (b) => a + b;
}

const add5 = curriedAdd(5);
console.log(add5(3));  // 8
console.log(add5(10)); // 15

// より複雑なカリー化
function curriedMultiply(a: number): (b: number) => (c: number) => number {
  return (b) => (c) => a * b * c;
}

const multiplyBy2 = curriedMultiply(2);
const multiplyBy2And3 = multiplyBy2(3);
console.log(multiplyBy2And3(4)); // 24 (2 * 3 * 4)

ジェネリックな関数ファクトリ

ジェネリクスを使って汎用的な関数ファクトリを作成できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// プロパティアクセサを生成するファクトリ
function createGetter<T, K extends keyof T>(key: K): (obj: T) => T[K] {
  return (obj) => obj[key];
}

interface Person {
  name: string;
  age: number;
  email: string;
}

const getName = createGetter<Person, "name">("name");
const getAge = createGetter<Person, "age">("age");

const person: Person = { name: "Alice", age: 30, email: "alice@example.com" };
console.log(getName(person)); // Alice
console.log(getAge(person));  // 30

// 配列に対してmapで使用
const people: Person[] = [
  { name: "Alice", age: 30, email: "alice@example.com" },
  { name: "Bob", age: 25, email: "bob@example.com" },
];

const names = people.map(getName);
console.log(names); // ["Alice", "Bob"]

合成関数(compose)の型定義

関数を合成する高階関数の型定義は複雑ですが、TypeScriptで実現できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 2つの関数を合成する
function compose<A, B, C>(f: (b: B) => C, g: (a: A) => B): (a: A) => C {
  return (a) => f(g(a));
}

// 使用例
const addOne = (x: number): number => x + 1;
const double = (x: number): number => x * 2;
const toString = (x: number): string => `Result: ${x}`;

// (x) => toString(double(x)) と同等
const doubleAndStringify = compose(toString, double);
console.log(doubleAndStringify(5)); // "Result: 10"

// 3つの関数を合成
const pipeline = compose(toString, compose(double, addOne));
console.log(pipeline(5)); // "Result: 12" ((5 + 1) * 2 = 12)

パイプライン関数の型定義

左から右に関数を適用するパイプラインパターンも一般的です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// pipe関数(2引数版)
function pipe<A, B, C>(f: (a: A) => B, g: (b: B) => C): (a: A) => C {
  return (a) => g(f(a));
}

// 使用例
const addOne = (x: number): number => x + 1;
const double = (x: number): number => x * 2;

const addOneThenDouble = pipe(addOne, double);
console.log(addOneThenDouble(5)); // 12 ((5 + 1) * 2)

// オーバーロードを使った可変長pipe
function pipeAll<A, B>(f1: (a: A) => B): (a: A) => B;
function pipeAll<A, B, C>(f1: (a: A) => B, f2: (b: B) => C): (a: A) => C;
function pipeAll<A, B, C, D>(f1: (a: A) => B, f2: (b: B) => C, f3: (c: C) => D): (a: A) => D;
function pipeAll(...fns: Array<(arg: unknown) => unknown>): (arg: unknown) => unknown {
  return (arg) => fns.reduce((acc, fn) => fn(acc), arg);
}

const toString = (x: number): string => `Value: ${x}`;
const process = pipeAll(addOne, double, toString);
console.log(process(5)); // "Value: 12"

thisの型付け

thisパラメータの基本

TypeScriptでは、関数の最初のパラメータとしてthisを宣言することで、thisの型を明示できます。このパラメータは実行時には存在せず、コンパイル時の型チェックにのみ使用されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
interface User {
  name: string;
  greet(this: User): string;
}

const user: User = {
  name: "Alice",
  greet() {
    // thisはUser型として推論される
    return `Hello, I'm ${this.name}`;
  },
};

console.log(user.greet()); // Hello, I'm Alice

// thisが別のオブジェクトにバインドされるとエラー
// const greetFn = user.greet;
// greetFn(); // Error: The 'this' context of type 'void' is not assignable to method's 'this' of type 'User'

コールバックにおけるthisの型付け

コールバック関数内でthisを使用する場合、その型を明示する必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
interface EventEmitter {
  name: string;
  on(event: string, callback: (this: EventEmitter, data: string) => void): void;
  emit(event: string, data: string): void;
}

function createEventEmitter(name: string): EventEmitter {
  const listeners: Map<string, Array<(this: EventEmitter, data: string) => void>> = new Map();

  return {
    name,
    on(event, callback) {
      const existing = listeners.get(event) || [];
      existing.push(callback);
      listeners.set(event, existing);
    },
    emit(event, data) {
      const callbacks = listeners.get(event) || [];
      callbacks.forEach((cb) => cb.call(this, data));
    },
  };
}

const emitter = createEventEmitter("MyEmitter");

emitter.on("message", function (data) {
  // thisはEventEmitter型として推論される
  console.log(`[${this.name}] Received: ${data}`);
});

emitter.emit("message", "Hello!"); // [MyEmitter] Received: Hello!

ThisParameterType と OmitThisParameter

TypeScriptには、thisパラメータを操作するユーティリティ型が用意されています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// thisパラメータを持つ関数
function greet(this: { name: string }, greeting: string): string {
  return `${greeting}, ${this.name}!`;
}

// ThisParameterType: this の型を抽出
type GreetThisType = ThisParameterType<typeof greet>;
// GreetThisType = { name: string }

// OmitThisParameter: this を除いた関数型を取得
type GreetWithoutThis = OmitThisParameter<typeof greet>;
// GreetWithoutThis = (greeting: string) => string

// bindを使ってthisを固定した関数を作成
const alice = { name: "Alice" };
const aliceGreet: GreetWithoutThis = greet.bind(alice);

console.log(aliceGreet("Hello")); // Hello, Alice!

クラスメソッドとthisの型

クラスのメソッドをコールバックとして渡す場合、thisのバインディングに注意が必要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Counter {
  count = 0;

  // アロー関数プロパティ: thisが自動的にバインドされる
  increment = (): void => {
    this.count++;
    console.log(`Count: ${this.count}`);
  };

  // 通常のメソッド: thisのバインディングが必要
  decrement(): void {
    this.count--;
    console.log(`Count: ${this.count}`);
  }
}

const counter = new Counter();

// アロー関数プロパティはそのまま渡せる
setTimeout(counter.increment, 100); // Count: 1

// 通常のメソッドはbindが必要
setTimeout(counter.decrement.bind(counter), 200); // Count: 0

// または、ラッパー関数を使用
setTimeout(() => counter.decrement(), 300); // Count: -1

実践的なユースケース

イベントハンドラの型定義

Webアプリケーションでよく使われるイベントハンドラの型定義パターンです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// イベントハンドラの型定義
type EventHandler<E extends Event> = (event: E) => void;
type ClickHandler = EventHandler<MouseEvent>;
type InputHandler = EventHandler<InputEvent>;
type KeyHandler = EventHandler<KeyboardEvent>;

// イベントリスナー管理クラス
class EventManager {
  private listeners: Map<string, Array<EventHandler<Event>>> = new Map();

  on<E extends Event>(eventType: string, handler: EventHandler<E>): void {
    const existing = this.listeners.get(eventType) || [];
    existing.push(handler as EventHandler<Event>);
    this.listeners.set(eventType, existing);
  }

  off<E extends Event>(eventType: string, handler: EventHandler<E>): void {
    const existing = this.listeners.get(eventType) || [];
    const index = existing.indexOf(handler as EventHandler<Event>);
    if (index > -1) {
      existing.splice(index, 1);
    }
  }

  emit<E extends Event>(eventType: string, event: E): void {
    const handlers = this.listeners.get(eventType) || [];
    handlers.forEach((handler) => handler(event));
  }
}

// 使用例
const manager = new EventManager();

const handleClick: ClickHandler = (event) => {
  console.log(`Clicked at (${event.clientX}, ${event.clientY})`);
};

manager.on("click", handleClick);

非同期コールバックパターン

Promiseと組み合わせたコールバックパターンの型定義です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// コールバックをPromiseに変換するユーティリティ
type NodeStyleCallback<T> = (error: Error | null, result: T) => void;

function promisify<T>(
  fn: (callback: NodeStyleCallback<T>) => void
): () => Promise<T>;
function promisify<T, A>(
  fn: (arg: A, callback: NodeStyleCallback<T>) => void
): (arg: A) => Promise<T>;
function promisify<T, A, B>(
  fn: (arg1: A, arg2: B, callback: NodeStyleCallback<T>) => void
): (arg1: A, arg2: B) => Promise<T>;
function promisify(fn: (...args: unknown[]) => void): (...args: unknown[]) => Promise<unknown> {
  return (...args) => {
    return new Promise((resolve, reject) => {
      fn(...args, (error: Error | null, result: unknown) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      });
    });
  };
}

// コールバックスタイルの関数
function fetchUserById(id: number, callback: NodeStyleCallback<{ id: number; name: string }>): void {
  setTimeout(() => {
    if (id > 0) {
      callback(null, { id, name: "User" + id });
    } else {
      callback(new Error("Invalid ID"), null as never);
    }
  }, 100);
}

// Promiseスタイルに変換
const fetchUserByIdAsync = promisify(fetchUserById);

// async/awaitで使用可能
async function main(): Promise<void> {
  try {
    const user = await fetchUserByIdAsync(1);
    console.log(user); // { id: 1, name: "User1" }
  } catch (error) {
    console.error(error);
  }
}

main();

ミドルウェアパターン

Express.jsスタイルのミドルウェアパターンの型定義です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// リクエスト・レスポンスの型定義
interface Request {
  path: string;
  method: string;
  body?: unknown;
  user?: { id: number; name: string };
}

interface Response {
  status(code: number): Response;
  json(data: unknown): void;
  send(body: string): void;
}

// ミドルウェアとハンドラの型定義
type NextFunction = () => void;
type Middleware = (req: Request, res: Response, next: NextFunction) => void;
type RouteHandler = (req: Request, res: Response) => void;

// ミドルウェアチェーンを実行する関数
function executeMiddleware(
  middlewares: Middleware[],
  req: Request,
  res: Response,
  finalHandler: RouteHandler
): void {
  let index = 0;

  const next: NextFunction = () => {
    if (index < middlewares.length) {
      const middleware = middlewares[index++];
      middleware(req, res, next);
    } else {
      finalHandler(req, res);
    }
  };

  next();
}

// ミドルウェアの例
const authMiddleware: Middleware = (req, res, next) => {
  // 認証処理のシミュレーション
  req.user = { id: 1, name: "Alice" };
  console.log("Auth middleware executed");
  next();
};

const loggingMiddleware: Middleware = (req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next();
};

// ルートハンドラ
const userHandler: RouteHandler = (req, res) => {
  res.json({ user: req.user });
};

// 実行例(モックレスポンス)
const mockRes: Response = {
  status(code) {
    console.log(`Status: ${code}`);
    return this;
  },
  json(data) {
    console.log("JSON:", JSON.stringify(data));
  },
  send(body) {
    console.log("Body:", body);
  },
};

executeMiddleware(
  [loggingMiddleware, authMiddleware],
  { path: "/users", method: "GET" },
  mockRes,
  userHandler
);
// 出力:
// GET /users
// Auth middleware executed
// JSON: {"user":{"id":1,"name":"Alice"}}

デコレータパターン

関数を装飾するデコレータパターンの型定義です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 関数デコレータの型
type Decorator<T extends (...args: unknown[]) => unknown> = (fn: T) => T;

// ロギングデコレータ
function withLogging<T extends (...args: unknown[]) => unknown>(fn: T): T {
  return ((...args: unknown[]) => {
    console.log(`Calling ${fn.name} with args:`, args);
    const result = fn(...args);
    console.log(`${fn.name} returned:`, result);
    return result;
  }) as T;
}

// メモ化デコレータ
function withMemoization<T extends (...args: unknown[]) => unknown>(fn: T): T {
  const cache = new Map<string, ReturnType<T>>();
  
  return ((...args: unknown[]) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      console.log("Cache hit for:", key);
      return cache.get(key);
    }
    const result = fn(...args) as ReturnType<T>;
    cache.set(key, result);
    return result;
  }) as T;
}

// 使用例
function fibonacci(n: number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const memoizedFib = withMemoization(withLogging(fibonacci));
console.log(memoizedFib(10)); // 55

まとめ

本記事では、TypeScriptにおけるコールバック関数と高階関数の型定義について詳しく解説しました。

学んだ内容

項目 内容
コールバック型の定義 型エイリアス、インライン定義、ジェネリック型
高階関数の型推論 map、filter、reduceの型推論の仕組み
関数を返す関数 カリー化、ファクトリ関数、compose/pipe
thisの型付け thisパラメータ、ThisParameterType、OmitThisParameter
実践パターン イベントハンドラ、ミドルウェア、デコレータ

ベストプラクティス

  1. 複雑なコールバック型は型エイリアスで定義する: 可読性と再利用性が向上します
  2. ジェネリクスを活用する: 汎用的で型安全な高階関数を作成できます
  3. 型ガードでfilterの戻り値を絞り込む: より正確な型推論が得られます
  4. thisパラメータを明示する: メソッドのthisバインディングエラーを防げます
  5. アロー関数プロパティを活用する: コールバックとして渡す際のthisの問題を回避できます

コールバック関数と高階関数の型定義をマスターすることで、TypeScriptの関数型プログラミングをより型安全に実践できるようになります。次の記事では、ユニオン型とリテラル型について詳しく解説します。

参考リンク