ゴール: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;
}
TypeScriptPredicate<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
TypeScript3つを並べて「関数型の違い」を言葉で説明してみる
それぞれの「コールバックの型」を比較する
ここまでの話を、あえて並べてみます。
map 用:
(value: T, index: number, array: T[]) => U
// T を受け取って U を返す
// 戻り値 U が配列の要素型になる
TypeScriptfilter 用:
(value: T, index: number, array: T[]) => boolean
// T を受け取って boolean を返す
// true の要素だけが残る。型は T のまま
TypeScriptreduce 用:
(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 は「便利なメソッド」から、
「型で設計された、小さくて強力な高階関数」 に見えてきます。
