React19で変化したuseReducerの型から学ぶTypeScriptの型推論
@types/reactのバージョン19ではuseReducerの型の変更が行われました。これまではreducer関数から状態の型を推論していましたが、バージョン19からはreducer関数と初期値から状態の型を推論する形に変更されました。この記事では、この変更を元にTypeScriptにおける型の推論方法について提案します。
はじめに
@types/react
のバージョン19では、React本体の変更に対する追従の他に幾つかの変更が加えられました(Changelog)。
その中の1つとしてuseReducer
の型の変更があります。
useReducer
の型の変更は型推論の向上を目的として行われました。
これまではuseReducer
を用いるためにreducer
関数から状態の型を推論していました。しかし、バージョン19からは初期値を含めた引数全体から型を推論する形に変更されました。
これにより、これまで通りの使い心地を保ちつつ、冗長な型定義を防ぐことに成功しました。
以下の例は、型の推論方法の変更によって、React18のuseReducer
で型エラーの発生する記述が、React19で起きないようになったものです。
// React18
// [any, Dispatch<Action>]
const [state, dispatch] = useReducer(
// 💣 Parameter 'state' implicitly has an 'any' type.
(prevCount) => prevCount + 1,
0,
);
// React19
// [number, ActionDispatch<[]>]
const [state, dispatch] = useReducer(
// prevCount: number
(prevCount) => prevCount + 1,
0,
);
この記事では、React19におけるuseReducer
の型定義の変更点に焦点を当て、以前は型エラーとなっていたケースがどのように解消されたのかを解説します。
さらに、これらの変化を紐解きながら、TypeScriptにおける型の推論方法について考察します。
useReducer
useReducer
はReactの状態を管理する機能の1つです(状態について)。
状態を更新する方法を指定するreducer
関数と初期値を指定する値を引数に取り、状態と状態を更新するための関数を配列で返します。
const [state, dispatch] = useReducer(reducer, {
id: 1,
name: 'k8o',
age: 26,
});
reducer
関数は状態の更新方法を指定するので、古い状態とそれに対する指示を引数に取り、新しい状態を返します。
type Action = { type: 'age-increment' } | { type: 'age-decrement' };
const reducer = (state: State, action: Action): State => {
const type = action.type;
switch (type) {
case 'age-increment':
return { ...state, age: state.age + 1 };
case 'age-decrement':
return { ...state, age: state.age - 1 };
default:
throw new Error(type satisfies never);
}
};
上記のreducer
関数は、年齢を1歳増やす更新方法と年齢を1歳減らす更新方法を持ちます。
State
型は状態の型で、Action
型は更新方法を指示する値の型です。
このreducer
関数を持つuseReducer
が返すdispatch
関数は、Action
型の値を引数に渡して実行します。
実行するとreducer
関数に定義した方法で状態を更新します。
const [state, dispatch] = useReducer(reducer, {
id: 1,
name: 'k8o',
age: 26,
});
const increment = () => {
dispatch({ type: 'age-increment' });
};
const decrement = () => {
dispatch({ type: 'age-decrement' });
};
この例ではid
やname
の更新方法を提供されていません。そのため、age
を1歳増やす、減らす以外の更新ができません。
このように、useReducer
はreducer
関数で指定した更新方法でしか状態を更新できないので、厳密な状態管理が可能になります。
初期値の与え方
useReducer
を使うときの初期値を設定する方法は2つの方法があります。
1.初期値を直接渡す方法
const [state, dispatch] = useReducer(reducer, initialArg);
これは一番シンプルな方法です。initialArg
がそのまま最初の状態になります。
2.「初期値を作るための関数」を渡す方法
const [state, dispatch] = useReducer(reducer, initialArg, init);
こちらの方法では、initialArg
を材料にしてinit
という関数が実行され、その結果init(initialArg)
が最初の状態になります。
2つ目の方法がある理由
1つ目の方法でも関数による計算結果を初期値にできます。
const [state, dispatch] = useReducer(reducer, init(initialArg));
実際に、この方法で返されるstate
とdispatch
は2つ目の方法と全く同じです。
1つ目の方法で記述できるのであれば、2つ目の方法は不要のようにも見えます。
それにもかかわらず、2つ目の方法がある理由はコンポーネントの初回の計算以降のinit(initialArg)
の実行を避けるためにあります。
1つ目の方法では引数にinit(initialArg)
を記述しているので、コンポーネントを計算するたびにinit(initialArg)
が実行されてしまいます。
しかし、2つ目の方法はinit
とinitialArg
を別々で渡しているので、Reactは初回の計算でのみinit(initialArg)
を実行するように最適化してくれます。
init
関数が複雑で時間のかかるものだった場合、コンポーネントの再計算のたびにその重い処理が何度も実行され、パフォーマンス低下に繋がります。
そのようなケースでは2つ目の方法が有効です。
このように、useReducer
はパフォーマンスへの配慮から、初期値を効率的に設定するための方法を提供しています。
useReducerをさらに詳しく知りたい場合
最後に少しReact寄りの説明をしましたが、この記事ではReactの哲学に則った説明を最小限にし、単純な関数としての性質だけを説明しました。 詳しく知りたい場合は、React公式のドキュメントが丁寧に解説していますので、そちらを読むことをおすすめします。
型の変更
さて、useReducer
の基本的な振る舞いを確認したところで、@types/react
に書かれたuseReducer
の型を見ていきましょう。
ここで紹介する型は@types/react
バージョン19.1.4からuseReducer
に関する型を抜粋したものです(React19の型、React18の型)。
それぞれの型の動きを手元で確認したい型のためにTypeScriptのPlaygroundでuseReducer1
とuseReducer2
として確認できる環境を用意しました。
React18
React18のuseReducer
の型は以下のようになっています(namespace
は適当です)。
declare namespace React18 {
type Dispatch<A> = (value: A) => void;
type DispatchWithoutAction = () => void;
type Reducer<S, A> = (prevState: S, action: A) => S;
type ReducerWithoutAction<S> = (prevState: S) => S;
type ReducerState<R extends Reducer<any, any>> =
R extends Reducer<infer S, any> ? S : never;
type ReducerAction<R extends Reducer<any, any>> =
R extends Reducer<any, infer A> ? A : never;
type ReducerStateWithoutAction<
R extends ReducerWithoutAction<any>,
> = R extends ReducerWithoutAction<infer S> ? S : never;
function useReducer<R extends ReducerWithoutAction<any>, I>(
reducer: R,
initializerArg: I,
initializer: (arg: I) => ReducerStateWithoutAction<R>,
): [ReducerStateWithoutAction<R>, DispatchWithoutAction];
function useReducer<R extends ReducerWithoutAction<any>>(
reducer: R,
initializerArg: ReducerStateWithoutAction<R>,
initializer?: undefined,
): [ReducerStateWithoutAction<R>, DispatchWithoutAction];
function useReducer<R extends Reducer<any, any>, I>(
reducer: R,
initializerArg: I & ReducerState<R>,
initializer: (arg: I & ReducerState<R>) => ReducerState<R>,
): [ReducerState<R>, Dispatch<ReducerAction<R>>];
function useReducer<R extends Reducer<any, any>, I>(
reducer: R,
initializerArg: I,
initializer: (arg: I) => ReducerState<R>,
): [ReducerState<R>, Dispatch<ReducerAction<R>>];
function useReducer<R extends Reducer<any, any>>(
reducer: R,
initialState: ReducerState<R>,
initializer?: undefined,
): [ReducerState<R>, Dispatch<ReducerAction<R>>];
}
最初にあるいくつかの型は、関数の型を表現するために利用する共通の型です。
Dispatch
型は戻り値の型で、DispatchWithoutAction
型はreducer
関数に状態の更新方法を指示する引数がない時の型です。
type Dispatch<A> = (value: A) => void;
type DispatchWithoutAction = () => void;
reducer
関数に状態の更新方法を指示する引数がない時の例を挙げると、以下のような形になります。
const [count, dispatch] = useReducer(
(prevCount: number) => prevCount + 1,
0,
);
const increment = () => {
dispatch();
};
このケースでは、Action
(reducer
関数の第2引数)がないので状態の更新方法が1通りしかないです。今後このケースは単にAction
がないケースと呼びます。
Reducer
型は状態を更新するための関数の型で、ReducerWithoutAction
型はAction
がない時の型です。
type Reducer<S, A> = (prevState: S, action: A) => S;
type ReducerWithoutAction<S> = (prevState: S) => S;
ReducerState
型はReducer
型から状態の型を抽出するための型で、ReducerStateWithoutAction
型はReducerWithoutAction
型から状態の型を抽出するための型です。
type ReducerState<R extends Reducer<any, any>> =
R extends Reducer<infer S, any> ? S : never;
type ReducerStateWithoutAction<R extends ReducerWithoutAction<any>> =
R extends ReducerWithoutAction<infer S> ? S : never;
ReducerAction
型はReducer
型からAction
型を抽出するための型です。
type ReducerAction<R extends Reducer<any, any>> =
R extends Reducer<any, infer A> ? A : never;
続いて、useReducer
自体の型を見ていきます。
useReducer
はオーバーロード関数で5つに分けて実装されています。
1つ目はAction
がなく、初期化関数を持つものです。
function useReducer<R extends ReducerWithoutAction<any>, I>(
reducer: R,
initializerArg: I,
initializer: (arg: I) => ReducerStateWithoutAction<R>,
): [ReducerStateWithoutAction<R>, DispatchWithoutAction];
2つ目はAction
がなく、初期化関数も持たないものです。
function useReducer<R extends ReducerWithoutAction<any>>(
reducer: R,
initializerArg: ReducerStateWithoutAction<R>,
initializer?: undefined,
): [ReducerStateWithoutAction<R>, DispatchWithoutAction];
3つ目、4つ目の関数はAction
を持ち、初期化関数も持つものです。
3つ目はuseReducer
の2つ目の型引数のI
型とReducerState
型の交差型を初期化関数に渡すもので、4つ目はI
型だけを初期化関数に渡すものです。
function useReducer<R extends Reducer<any, any>, I>(
reducer: R,
initializerArg: I & ReducerState<R>,
initializer: (arg: I & ReducerState<R>) => ReducerState<R>,
): [ReducerState<R>, Dispatch<ReducerAction<R>>];
function useReducer<R extends Reducer<any, any>, I>(
reducer: R,
initializerArg: I,
initializer: (arg: I) => ReducerState<R>,
): [ReducerState<R>, Dispatch<ReducerAction<R>>];
5つ目の関数はAction
を持ち、初期化関数を持たないものです。
function useReducer<R extends Reducer<any, any>>(
reducer: R,
initialState: ReducerState<R>,
initializer?: undefined,
): [ReducerState<R>, Dispatch<ReducerAction<R>>];
どの関数もreducer
関数からReducer<S, A>
型かReducerWithoutAction<S>
型を推論して、そこから状態の型やAction
型、Dispatch<ReducerAction<R>>
型を求めています。
React19
React19のuseReducer
の型は以下のようになっています。
React18の型と比べて、オーバーロード関数の数が2つまで減って、読みやすくなりました。
declare namespace Reducer {
type AnyActionArg = [] | [any];
type ActionDispatch<ActionArg extends AnyActionArg> = (
...args: ActionArg
) => void;
function useReducer<S, A extends AnyActionArg>(
reducer: (prevState: S, ...args: A) => S,
initialState: S,
): [S, ActionDispatch<A>];
function useReducer<S, I, A extends AnyActionArg>(
reducer: (prevState: S, ...args: A) => S,
initialArg: I,
init: (i: I) => S,
): [S, ActionDispatch<A>];
}
AnyActionArg
型はAction
自体、ActionDispatch
型は更新関数の型です。
type AnyActionArg = [] | [any];
type ActionDispatch<ActionArg extends AnyActionArg> = (
...args: ActionArg
) => void;
1つ目の関数は初期化関数を持たない場合に適用される型です。
function useReducer<S, A extends AnyActionArg>(
reducer: (prevState: S, ...args: A) => S,
initialState: S,
): [S, ActionDispatch<A>];
2つ目の関数は初期化関数を持つ場合に適用されます。
function useReducer<S, I, A extends AnyActionArg>(
reducer: (prevState: S, ...args: A) => S,
initialArg: I,
init: (i: I) => S,
): [S, ActionDispatch<A>];
React19では、reducer
関数から各型を推論するのではなく、状態の型(S
型)とAction
型(A
型)を推論してから各型の推論するようになりました。
明示的な型付け方法の変更
React19になって、useReducer
の受け取る型引数が変更されました。
React18ではreducer
関数の型(Reducer<S, A>
型かReducerWithoutAction<S>
型)を渡していたところから、React19では状態の型(S
型)とAction
型(AnyActionArg
型)を渡すように変更されました。
// React18
const [state, dispatch] = useReducer<Reducer<State, Action>>(
reducer,
initialArg,
init,
);
const [state, dispatch] = useReducer<Reducer<State, Action>, I>(
reducer,
initialArg,
init,
);
// React19
const [state, dispatch] = useReducer<State, [Action]>(
reducer,
initialArg,
init,
);
const [state, dispatch] = useReducer<State, I, [Action]>(
reducer,
initialArg,
init,
);
初期化関数を用いるときに利用する型引数I
型を渡す順番には違和感を覚えますが、内部の型を使わずに型を渡せるようになったのは良い変化だと感じました。
なお、React19ではuseReducer
に型引数を渡さないことがベストプラクティスとされているので、推論でカバーできないときにのみ型引数を利用するようにしましょう。
The new best practice is not to pass type arguments to useReducer.
型の推論の向上
はじめに紹介したReact18では型推論がうまく働かず、型エラーが発生してしまう例についてです。
このエラーは、React18のuseReducer
ではreducer
関数からのみ状態の型を推論することが原因で起きています。reducer
関数の型が不十分なので、状態の型を推論する方法がなく、型エラーが生じていたのです。
そのため、React18ではreducer
関数が型を持っていない状態で、useReducer
の宣言はできませんでした。
type Action = { type: 'age-increment' } | { type: 'age-decrement' };
// [any, Dispatch<Action>]
const [state, dispatch] = useReducer(
// 💣 Parameter 'state' implicitly has an 'any' type.
(state, action: Action) => {
const type = action.type;
switch (type) {
case 'age-increment':
return { ...state, age: state.age + 1 };
case 'age-decrement':
return { ...state, age: state.age - 1 };
default:
throw new Error(type satisfies never);
}
},
{
id: 1,
name: 'k8o',
age: 26,
},
);
React19では状態の型を初期値からも推論できるの問題なく宣言できます。
// [
// {
// id: number,
// name: string,
// age: number
// },
// ActionDispatch<[action: Action]>,
// ]
const [state, dispatch] = useReducer(
(state, action: Action) => {
const type = action.type;
switch (type) {
case 'age-increment':
return { ...state, age: state.age + 1 };
case 'age-decrement':
return { ...state, age: state.age - 1 };
default:
throw new Error(type satisfies never);
}
},
{
id: 1,
name: 'k8o',
age: 26,
},
);
これは、React19のuseReducer
が状態の型をreducer
関数だけに頼らず、初期値からも推論するように変更されたためです。
実は、React19におけるuseReducer
の型が変更はこの変更のために行われたものです。
Improve [react] useReducer type inference · DefinitelyTyped/DefinitelyTyped · Discussion #63607 · GitHub
The problem useReducer does not infer the state parameter from the initial state passed in: const [state, dispatch] = useReducer( (state, action: Action) => { // ... }, 0, ); This should infer stat...
github.com
useReducerから学ぶ型推論
React18からReact19のuseReducer
の型変更から、関数を引数にもつ関数の型を定義する方法を学ぶことができます。
React18では、reducer
関数という主体となる型があり、そこから状態の型やAction
型を推論する形でした。
一方、React19では状態の型とAction
型を主体として、そこからreducer
関数の型やActionDispatch
型を推論する形になりました。

これを一般化して、以下のように定義します。
- React18式: 主体となる型があり、それから従属する型を推論させる方法
- React19式: 値の主従関係を決めずに基本的な方から型を推論させる方法

React19式は型の定義が簡潔で可読性も高くなりやすく利用しやすい使いやすい型になっていますが、React18式は値の主従関係を型に持ち込むような複雑な定義となります。
そのため、React19式は汎用的に使い、React18式は型の上でも主従関係を明確にした強い制約を持たせたい場合に有効です。React19式だけが良いわけではなく、React18式も有効なケースがある事に注意が必要です。
配列をフィルタする関数を例に
例として、配列をフィルタする関数を考えます。
type Filter<T> = (value: T) => boolean;
type Value<F extends Filter<any>> =
F extends Filter<infer U> ? U : never;
// React18版
const arrayFilter1 = <F extends Filter<any>>(
array: Value<F>[],
filter: F,
): Value<F>[] => {
return array.filter(filter);
};
// React19版
const arrayFilter2 = <T>(array: T[], filter: Filter<T>): T[] => {
return array.filter(filter);
};
arrayFilter1
はReact18式の型定義で、arrayFilter2
はReact19式の型定義です。
それぞれの関数の型は以下のように推論されます。
// result1: any[]
const result1 = arrayFilter1(
[1, 2, 3],
// val: any
(val) => val > 0,
);
// result1: number[]
const result1 = arrayFilter1<Filter<number>>(
[1, 2, 3],
// val: number
(val) => val > 0,
);
// result2: number[]
const result2 = arrayFilter2(
[1, 2, 3],
// val: number
(val) => val > 0,
);
arrayFilter1
は関数から型を推論しているので、関数に型情報を付与するもしくは、型引数を渡さないと型がany[]
になってしまいます。
一方、arrayFilter2
は配列からも型を推論しているので、型引数を渡さなくても型が特定できるようになっています。
例に挙げた関数であれば、フィルタ関数を主役と考えてフィルタ関数が対応する型のみを配列の値に許可させたい場合はarrayFilter1
のように、配列の値からも値の型を推論可能にして作用させるフィルタ関数の型を推論させたい場合はarrayFilter2
のようにするのが良いでしょう。
React18式が有効なケース
React18式が有効なケースを紹介します。
ここでは、部分適用を実装した関数を紹介します。部分適用とはある関数から引数の一部分を代入した状態の関数を作り出すような操作を意味します(厳密な定義ではない)。
この関数は適用元の関数が存在する上で代入する値が存在すると考えられます。そのため、部分適用の制約を型の上でも表現したい場合はReact18式が有効です。
type TargetFunc<T, U extends any[], K> = (arg: T, ...args: U) => K;
type FirstArg<T> = T extends (arg: infer U, ...args: never) => void
? U
: never;
type RestArg<T> = T extends (arg: never, ...args: infer U) => void
? U
: never;
const partial = <F extends TargetFunc<any, any, any>>(
func: F,
value: FirstArg<F>,
): ((...rest: RestArg<F>) => ReturnType<F>) => {
return (...rest) => func(value, ...rest);
};
const plus3 = partial((num1: number, num2: number) => num1 + num2, 3);
plus3
は、2つの数値を足し合わせる関数をベースに、一方の数値を3へ固定した関数になります。
この関数は適用元の関数が主役となるように作られているので、適用元の関数に型がない場合は型エラーが発生します。 また、適用元の関数が受け取れない値を第二引数に渡したときは第二引数だけにエラーが発生します。
これらのエラーは適用元の関数を主として考える上であえて発生させた(ある種リンターのような意味合いを持つ)ものです。このような制約をあえて設けたいケースでは、React18式を用いることがおすすめです。
React19式が使われるケース
React19式が使われるケースは巷にあふれています。
例えば配列に実装されているreduce
メソッドは、配列の要素から型を推論する形で実装されています。
// TypeScriptの定義を抽出したもの
interface Array<T> {
reduce<U>(
callbackfn: (
previousValue: U,
currentValue: T,
currentIndex: number,
array: T[],
) => U,
initialValue: U,
): U;
}
// sum: number, prev: number, curr: number
const sum1 = [1, 2, 3].reduce((prev, curr) => prev + curr, 0);
// sum: number, prev: number, curr: number
const sum2 = [1, 2, 3].reduce(
(prev: number, curr: number) => prev + curr,
0,
);
sum1
の計算では、callbackfn
の引数であるpreviousValue
とcurrentValue
の型は配列と初期値から推論しています。
そして、sum2
の計算では、callbackfn
の引数であるpreviousValue
とcurrentValue
の型は明示的に指定しています。この時は、配列、初期値、previousValue
、currentValue
の型に矛盾ないことが求められます。
previousValue
とcurrentValue
が関数の型を決定するだけではなく、配列と初期値からも型を決定できる柔軟な型の実装になっています。
これが、React19式の便利で汎用性の高いところです。このようにReact19式は柔軟で使いやすい形なので、ほとんどのパターンではReact19式で実装するのが良いでしょう。
この考えを元にすると、useReducer
はreducer
に従って状態が決まるわけではないので、React19におけるuseReducer
の型の変更は良い変更だと考えられます。
さらに、useReducer
は配列のreduce
関数に因んで名付けられているので、そういう意味でも配列のreduce
に近い型の実装になったのは良いことだと思いました(なぜリデューサと呼ばれるのか?)。
おわりに
React19では、useReducer
の型が変更され、よりシンプルで使いやすくなりました。特に、型推論の向上により、冗長な型定義を避けることができるようになりました。
この変更は、useReducer
を使用するReact・TypeScriptユーザーにとって、より快適な開発体験をもたらせてくれました。
さらに、React19の型変更を通じて、TypeScriptの型推論について考察しました。作成したい型の振る舞いを考え、React18式の型とReact19式の型を使い分けることで、より柔軟で堅牢な型定義が可能になります。
今回の考察は日頃、身近で複雑な機能の型を実装して実際の型と比較することで生まれました。皆さんも普段ライブラリで利用するような機能の型について思いを馳せてみてはいかがでしょうか。