TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – map / filter / reduce 用関数型

TypeScript
スポンサーリンク

ゴール:map / filter / reduce の「関数型」を言葉で説明できるようになる

まず目標からはっきりさせます。

map / filter / reduce は、
「配列に対して“どう変換するか・どう絞り込むか・どう畳み込むか”を、関数で渡す仕組み」です。

ここで大事なのは、

「その“渡す関数”が、どんな引数を受け取って、何を返すべきか」

を、型としてきちんと理解することです。

これが分かると、

自分で似た関数を実装できる
コールバックの型を type で定義して再利用できる
型エラーの意味が直感的に分かる

というところまで一気に進めます。


map 用の関数型:「1つ受け取って、1つ返す変換」

map の基本的なイメージ

map は「配列の各要素を、別の値に変換して、新しい配列を作る」関数です。

const numbers = [1, 2, 3];

const doubled = numbers.map((n) => n * 2);
// doubled: number[]
TypeScript

ここで map に渡している関数は、

「1つの要素を受け取って、変換した値を返す関数」

です。

map のコールバック関数型を分解する

Array.prototype.map のコールバックの型は、ざっくりこうです。

(value: T, index: number, array: T[]) => U
TypeScript

これを言葉にすると、

「要素 value(型 T)、インデックス index、元の配列 array を受け取って、
結果 U を返す関数」

です。

一番よく使うのは、先頭の value と戻り値だけです。

const names = ["Taro", "Hanako"];

const lengths = names.map((name): number => {
  return name.length;
});
// name: string
// 戻り値: number
// lengths: number[]
TypeScript

ここでの重要ポイントは、

「map の“戻り値の配列の要素型”は、
コールバックの“戻り値の型”そのもの」

ということです。

つまり、

元の配列の要素型:T
コールバックの戻り値型:U
map の戻り値型:U[]

という関係になっています。

map 用の関数型を自分で定義してみる

よく使うなら、型に名前をつけてしまうのもアリです。

type MapFn<T, U> = (value: T, index: number, array: T[]) => U;

function myMap<T, U>(array: T[], fn: MapFn<T, U>): U[] {
  const result: U[] = [];
  for (let i = 0; i < array.length; i++) {
    result.push(fn(array[i], i, array));
  }
  return result;
}
TypeScript

こうすると、

MapFn<T, U> は“map 用の関数型”なんだな」

と一目で分かるようになります。


filter 用の関数型:「1つ受け取って、true / false を返す判定」

filter の基本的なイメージ

filter は「条件に合う要素だけを残す」関数です。

const numbers = [1, 2, 3, 4, 5];

const evens = numbers.filter((n) => n % 2 === 0);
// evens: number[]
TypeScript

ここで渡している関数は、

「1つの要素を受け取って、その要素を“残すかどうか”を true / false で返す関数」

です。

filter のコールバック関数型を分解する

Array.prototype.filter のコールバックの型は、ざっくりこうです。

(value: T, index: number, array: T[]) => boolean
TypeScript

言葉にすると、

「要素 value(型 T)、インデックス index、元の配列 array を受け取って、
その要素を残すなら true、捨てるなら false を返す関数」

です。

一番よく使うのは、やはり value と戻り値だけ。

const words = ["apple", "banana", "cat"];

const longWords = words.filter((w) => w.length >= 5);
// longWords: string[]
TypeScript

ここでの重要ポイントは、

「filter の戻り値の配列の要素型は、元の配列と同じ T」

ということです。

map は「型を変える」
filter は「個数を減らすだけで、型は変えない」

という違いがあります。

filter 用の関数型を自分で定義してみる

これも型に名前をつけられます。

type Predicate<T> = (value: T, index: number, array: T[]) => boolean;

function myFilter<T>(array: T[], fn: Predicate<T>): T[] {
  const result: T[] = [];
  for (let i = 0; i < array.length; i++) {
    if (fn(array[i], i, array)) {
      result.push(array[i]);
    }
  }
  return result;
}
TypeScript

Predicate<T> という名前はよく使われます。
「T を受け取って true / false を返す“条件関数”」という意味です。


reduce 用の関数型:「蓄積値+要素を受け取って、新しい蓄積値を返す」

reduce の基本的なイメージ

reduce は「配列を1つの値に畳み込む」関数です。

const numbers = [1, 2, 3];

const sum = numbers.reduce((acc, n) => acc + n, 0);
// sum: number
TypeScript

ここで渡している関数は、

「今までの結果(蓄積値)と、現在の要素を受け取って、
新しい結果(蓄積値)を返す関数」

です。

reduce のコールバック関数型を分解する

Array.prototype.reduce のコールバックの型は、ざっくりこうです。

(accumulator: A, value: T, index: number, array: T[]) => A
TypeScript

ここで、

T は配列の要素型
A は蓄積値(accumulator)の型

です。

言葉にすると、

「蓄積値 accumulator(型 A)、要素 value(型 T)、インデックス、配列を受け取って、
新しい蓄積値(型 A)を返す関数」

です。

reduce 自体の型は、こういうイメージになります。

function reduce<T, A>(
  array: T[],
  fn: (acc: A, value: T, index: number, array: T[]) => A,
  initial: A
): A {
  let acc = initial;
  for (let i = 0; i < array.length; i++) {
    acc = fn(acc, array[i], i, array);
  }
  return acc;
}
TypeScript

ここでの超重要ポイントは、

「reduce の“戻り値の型”は、
コールバックの“蓄積値の型 A”と一致する」

ということです。

map は「要素 → 要素」
filter は「要素 → boolean」
reduce は「蓄積値+要素 → 蓄積値」

という構造になっています。

reduce 用の関数型を自分で定義してみる

型エイリアスにすると、さらに分かりやすくなります。

type Reducer<T, A> = (acc: A, value: T, index: number, array: T[]) => A;

function myReduce<T, A>(array: T[], fn: Reducer<T, A>, initial: A): A {
  let acc = initial;
  for (let i = 0; i < array.length; i++) {
    acc = fn(acc, array[i], i, array);
  }
  return acc;
}
TypeScript

使ってみます。

const sum = myReduce([1, 2, 3], (acc, n) => acc + n, 0);
// sum: number

const joined = myReduce(["a", "b", "c"], (acc, s) => acc + s, "");
// joined: string
TypeScript

3つを並べて「関数型の違い」を言葉で説明してみる

それぞれの「コールバックの型」を比較する

ここまでの話を、あえて並べてみます。

map 用:

(value: T, index: number, array: T[]) => U
// T を受け取って U を返す
// 戻り値 U が配列の要素型になる
TypeScript

filter 用:

(value: T, index: number, array: T[]) => boolean
// T を受け取って boolean を返す
// true の要素だけが残る。型は T のまま
TypeScript

reduce 用:

(acc: A, value: T, index: number, array: T[]) => A
// A と T を受け取って A を返す
// A が「畳み込み結果の型」になる
TypeScript

ここで一番大事なのは、

「map / filter / reduce の“違い”は、
コールバックの“戻り値の型”に全部現れている」

と気づくことです。

map:戻り値が「新しい要素」
filter:戻り値が「残すかどうかの判定」
reduce:戻り値が「次の蓄積値」

だから、
「コールバックの型をちゃんと書ける=その関数が何をしているかをちゃんと説明できる」
ということになります。


まとめ:map / filter / reduce 用関数型を自分の言葉で言うと

最後に、あなた自身の言葉でこう言えるか、試してみてください。

map 用の関数型は、

「配列の要素 T を受け取って、別の型 U を返す関数」
その戻り値 U が、新しい配列の要素型になる。

filter 用の関数型は、

「配列の要素 T を受け取って、true / false を返す関数」
true を返した要素だけが残り、型は T のまま。

reduce 用の関数型は、

「蓄積値 A と要素 T を受け取って、新しい蓄積値 A を返す関数」
その A が、最終的な戻り値の型になる。

そして、
「よく使うなら、MapFn / Predicate / Reducer みたいに型に名前をつけてしまうと、
コード全体の“意図”が一気に読みやすくなる。」

ここまで腹落ちしてくると、
map / filter / reduce は「便利なメソッド」から、
「型で設計された、小さくて強力な高階関数」 に見えてきます。

タイトルとURLをコピーしました