react-redux の Hooks API に Generics は要らない


react-redux でも、Hooks を利用した API が普及しましたね。この API を利用するうえで、型定義注入方法にコツがありますので共有します。1つのプロジェクトにつき、1つの Store のはずなので、その前提で話を進めます。

store.ts
export type StoreState = {
  hoge: { hoge: 'hoge' }
  fuga: { fuga: 'fuga' }
}

StoreState の型定義方法は数通りありますが、この様な型定義があることが前提です。

普通に書くとこうなる

この条件でuseSelectorを利用してみます。useSelectorの Generics に従い、都度StoreStateを import し、それを注入しています。なんだかあまりイケてません。

import { useSelector } from 'react-redux'
import { StoreState } from '../store'
const Container: React.FC = () => {
  // const _hoge: "hoge"
  const _hoge = useSelector<
    StoreState,
    StoreState['hoge']['hoge']
  >(state => state.hoge.hoge)
  return <Component _hoge={_hoge}>
}

求めている・実現できるもの

次の様にuseSelectorの Generics は指定しません。そして、StoreStateの import も不要です。それでいて、型推論がきちんと導かれている状態です。

import { useSelector } from 'react-redux'
const Container: React.FC = () => {
  // const _hoge: "hoge"
  const _hoge = useSelector(state => state.hoge.hoge)
  return <Component _hoge={_hoge}>
}

これは、実現することができます。

Ambient Module 宣言で overload する

StoreStateはライブラリが知ることのできない、プロジェクト固有の定義ですね。DefaultRootState という型定義が@types/react-redux内に用意されていますので確認してください(v7.1.7)。これを次の様に interface overload すれば、DefaultRootState を参照している内部の API 型定義に StoreState が行き渡る様になります。

import 'react-redux'
import { StoreState } from '../store'
// ______________________________________________________
//
declare module 'react-redux' {
  interface DefaultRootState extends StoreState {}
}

DefaultRootState の元定義は空っぽですが、この様な注入手法に向けてあらかじめ宣言されています。interface 宣言結合を利用したテクニックであり、様々なライブラリ型定義でも採用されています。この DefaultRootState も styled-components の DefaultTheme をインスパイアしたという旨のPRから生まれています。https://github.com/DefinitelyTyped/DefinitelyTyped/pull/41031

他の API にもプロジェクト固有の型定義を注入する

useDispatchuseStore を利用するたび、プロジェクト固有の型定義を import し Generics 注入するのはスマートではないので、こちらも対応します。

先のコードから新たにプロジェクト固有の型定義として追加しているのはActionsです。String Literal Type である typeプロパティで厳格に識別できるこの型は UnionTypes で表現されます。

store.ts
export type Actions = { type: "INCREMENT" } | { type: "DECREMENT" }

これらも次の様に Ambient Module 宣言していれば、普段型定義を意識しなくとも、useDispatchuseStore に型推論が適用されます(例えば、プロジェクトに存在しない Action を dispatch することを防ぐなど)。以下は"@types/react-redux": "7.1.7"時点で最適と思われる Ambient Module 宣言です。

import 'react-redux'
import { Store, Dispatch } from 'redux'
import { StoreState, Actions } from '../store'
// ______________________________________________________
//
declare module 'react-redux' {
  interface DefaultRootState extends StoreState {}
  export function useDispatch<TDispatch = Dispatch<Actions>>(): TDispatch
  export function useStore<S = DefaultRootState>(): Store<S, Actions>
}

プロジェクトにおいて1つしか存在しえないインスタンスは、この様に Ambient Module 宣言を積極的に活用しましょう。