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の定義に従い、都度StoreStateを import し、それを Generics に注入しています。なんだかあまりイケてません。

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内に用意されており、これを次の様に interface overload すれば、プロジェクトの StoreState が行き渡る様になります。

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

これで、一切の Generics 注入が不要になりました。equalityFnの引数も、第一引数関数(selector関数)が返す型によって変動することが確認できます。

他の 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 宣言を積極的に活用しましょう。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account