Help us understand the problem. What is going on with this article?

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

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
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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
ユーザーは見つかりませんでした