先に結論
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);