先に結論
Array.reduceの型定義には複数の種類があります。
interface Array<T> {
// 中略 一部抜粋
reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T;
reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U;
}
参考: https://github.com/microsoft/TypeScript/blob/v3.6.3/lib/lib.es5.d.ts#L1168
超おおざっぱに言うと、Array<T>.reduceが配列の要素と同じ型Tを返すか、そうでない型Uを返すかの推論の違いがあります。
意図した推論のされ方にならないとコンパイルエラーになる時があります。
例1: 配列の要素の型がreduceの初期値と同じ場合
下記は正しいTypeScriptのプログラムです。
function counter1(ary: number[]) {
// acc: number
return ary.reduce((acc, val) => {
return acc + val;
}, 0);
}
ここでは、Array<T>.reduce()が下記すべて同じ型Tだと推論しています。
-
reduce()のコールバック第一引数acc -
reduce()のコールバック第二引数val -
reduce()の返り値
// Array<T>.reduce の型定義(一部抜粋)
interface Array<T> {
reduce(fn: (previousValue: T, currentValue: T) => T, initialValue: T): T;
}
結果、関数内に型アノテーションが一切無くても型推論で型安全性が担保されコンパイルも通ります。
例2: 配列の要素の型がreduceの初期値と異なるが、互換性はある場合
引数の型をary: number[]からary: (number | undefined)[]と変更してみました。
するとObject is possibly 'undefined'.となってエラーとなります。
function counter2(ary: (number | undefined)[]) {
// acc: number | undefined と推論される
return ary.reduce((acc, val) => {
if (val === undefined) return acc;
return acc + val; // accで `Object is possibly 'undefined'.` エラー
}, 0);
}
Array<T>.reduce()のコールバック第一引数accの型が、配列の要素Tと同じ型number | undefinedだと推論されて、結果returnでエラーになっていることがわかります。
(ここでは、reduceの初期値と同じ型numberにしたい)
これはaccの型を明記してあげることで解決します。
function counter2Fixed1(ary: (number | undefined)[]) {
// acc: number
return ary.reduce((acc: number, val) => {
if (val === undefined) return acc;
return acc + val;
}, 0);
}
もしくは、Array<T>.reduce<U>()のようにジェネリクスでreduceに型numberを渡してあげても解決します。
function counter2Fixed2(ary: (number | undefined)[]) {
// acc: number
return ary.reduce<number>((acc, val) => {
if (val === undefined) return acc;
return acc + val;
}, 0);
}
reduce<U>に渡したUは、accumulatorとreduce自体の返り値の型として使われます。
// Array<T>.reduce<U> の型定義(一部抜粋)
interface Array<T> {
reduce<U>(fn: (previousValue: U, currentValue: T) => U, initialValue: U): U;
}
例3: 配列の要素の型がreduceの初期値と異なり、互換性が無い場合
下記は正しいプログラムです。
function counter3(ary: [string, (number | undefined)][]) {
// acc: number
return ary.reduce((acc, [_key, val]) => {
if (val === undefined) return acc;
return acc + val;
}, 0);
}
最初から、配列の要素の型[string, number | undefined]と初期値の型numberに互換性が無いため、Array<T>.reduce<U>()として推論されるようです。
結果、例2とは異なり型アノテーションが一切無くても型推論がちゃんと動作します。
TypeScript Playgroundで、上記の結果を試すことができます。
Object.valuesとObject.entriesの例
実際には、Object.values()とObject.entries()に対してそれぞれreduceを使った時にこのような違いが観測できます。
type FamilyAges = {
ichiro?: number,
jiro?: number,
saburo?: number
}
const ages: FamilyAges = {
ichiro: 11,
jiro: 22
}
// Object.values(ages): (number | undefined)[]
Object.values(ages).reduce((acc, val) => {
if (val === undefined) return acc;
return acc + val; // accで `Object is possibly 'undefined'.` エラー
}, 0);
// Object.entries(ages): [string, (number | undefined)][]
Object.entries(ages).reduce((acc, [_key, val]) => {
if (val === undefined) return acc;
return acc + val; // OK
}, 0);