LoginSignup
146
93

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-02-03

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

146
93
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
146
93