TypeScript | 基礎文法:配列・タプル – reduceの型指定

TypeScript
スポンサーリンク

reduceの「型指定」とは何を決めるものか

reduce は、配列を「1つの値」に畳み込むメソッドです。
TypeScript的に一番大事なのは、「途中経過(累積値:acc)の型をどう扱うか」です。
この「累積値の型」を、TypeScriptに推論させるのか、自分で明示的に指定するのか——それが「reduceの型指定」です。

const nums = [1, 2, 3]; // number[]

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

この例では、初期値 0number なので、accnumber、戻り値も number と推論されます。
ところが、「配列の要素型と累積値の型が違う」「初期値が空配列や空オブジェクト」になると、一気に難しくなります。


reduceの基本シグネチャと型推論の流れ

「初期値あり」のときの型の流れ

reduce のシグネチャをざっくり書くと、こういうイメージです。

array.reduce(
  (acc: A, cur: T) => A,
  initialValue: A
): A
TypeScript

ここで、

  • T は「配列の要素型」
  • A は「累積値(acc)の型」

です。

TypeScriptは、次の情報から A を推論しようとします。

  1. 初期値 initialValue の型
  2. コールバックの戻り値の型

たとえば、こうです。

const nums = [1, 2, 3]; // number[]

const sum = nums.reduce((acc, n) => acc + n, 0);
// acc: number, n: number, 戻り値: number → sum: number
TypeScript

ここでは、初期値 0number なので、accnumber、戻り値も number と推論され、
結果として sumnumber になります。


「要素型と違う型に畳み込みたい」ときの型指定

典型例:number[] をオブジェクトに畳み込む

const nums = [1, 2, 3]; // number[]

const map = nums.reduce((acc, n) => {
  acc[n] = `No.${n}`;
  return acc;
}, {} as Record<number, string>);
TypeScript

この書き方、現場でよく見ますが、実は「型キャスト(as)」に頼っていて、あまり良くありません。

{} の型はデフォルトだと {} で、インデックスシグネチャも何もない「ほぼ何もできない型」です。
それを as Record<number, string> とキャストして無理やり使っているので、
TypeScriptの型チェックを一部すり抜けてしまう可能性があります。

ここで効いてくるのが、「reduceにジェネリクス型パラメータを明示的に渡す」というテクニックです。

ジェネリクスで累積値の型を指定する

const nums = [1, 2, 3]; // number[]

const map = nums.reduce<Record<number, string>>((acc, n) => {
  acc[n] = `No.${n}`;
  return acc;
}, {});
// map: Record<number, string>
TypeScript

ここでやっていることは、

  • reduce<Record<number, string>> と書いて、「累積値の型は Record<number, string> だ」と宣言する
  • 初期値 {} は、その型の「空の値」として扱われる
  • コールバックの accRecord<number, string> として型チェックされる

という流れです。

これにより、

  • acc[123] = "x" のような操作はOK
  • 存在しないプロパティに変な型を入れようとするとエラー
  • キャスト(as)に頼らないので、型安全性が高い

という、かなり気持ちいい状態になります。


「空配列」「空オブジェクト」を初期値にするときの落とし穴

そのままだと never[] や {} になってしまう

const nums = [1, 2, 3];

const result = nums.reduce((acc, n) => {
  acc.push(n * 2);
  return acc;
}, []);
TypeScript

一見よくありそうなコードですが、[] の型はデフォルトだと never[] です。
「何も入っていないから、要素型が推論できない → 何も入れられない配列(never[])」という扱いになります。

その結果、acc.push(n * 2) のところで「never[] に要素は push できない」というエラーになりがちです。

これを避けるために、つい as number[] と書きたくなりますが、
さっきと同じく「キャスト頼み」になってしまいます。

reduceの型パラメータで素直に指定する

const nums = [1, 2, 3];

const result = nums.reduce<number[]>((acc, n) => {
  acc.push(n * 2);
  return acc;
}, []);
// result: number[]
TypeScript

ここで reduce<number[]> と書くことで、

  • 累積値 acc の型は number[]
  • 初期値 []number[] として扱われる
  • push も型安全に使える

という状態になります。

「空配列」「空オブジェクト」を初期値にする reduce は、
「型パラメータで累積値の型を明示する」のがベストプラクティスです。


reduceの型指定とジェネリクスの関係

reduceはジェネリック関数

reduce は内部的にジェネリック関数として定義されています。
ざっくり書くと、こんなイメージです。

reduce<A>(
  callbackfn: (previousValue: A, currentValue: T, index: number, array: T[]) => A,
  initialValue: A
): A
TypeScript

ここで A が「累積値の型」、T が「配列の要素型」です。

TypeScriptは通常、この A を「初期値」と「コールバックの戻り値」から推論しますが、
推論がうまくいかない(または危険な)ケースでは、
reduce<A> のように自分で A を指定してあげると、型が一気に安定します。

これは、TypeScriptのジェネリクスの一般的な使い方と同じで、
「推論に任せるか」「明示的に渡すか」を状況に応じて選ぶスタイルです。


初心者がまず押さえておきたい「reduceの型指定」の感覚

reduceの型指定で一番大事なのは、次の2つです。

  1. 「累積値(acc)の型は何か?」を意識すること
  2. 「初期値が空配列・空オブジェクトのときは、ジェネリクスで型を明示すること」

具体的には、

// 要素型と累積値の型が同じ → 推論に任せてOK
nums.reduce((acc, n) => acc + n, 0);

// 要素型と累積値の型が違う → reduce<累積値の型> を書く
nums.reduce<Record<number, string>>((acc, n) => { ... }, {});

nums.reduce<number[]>((acc, n) => { ... }, []);
TypeScript

という使い分けです。

「reduceを書くときは、まず“accの型”を頭の中で決める」
「空の値を初期値にするときは、型パラメータでちゃんと教えてあげる」

この2つの癖がつくと、reduceは一気に“怖いメソッド”から“型安全な変換マシン”に変わります。

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