Redux, Redux-Thunkを使った時のActionの型導出
目的は
const reducer = (state: State = initial, action: Action): State => {
...
}
の第2引数のActionの型をできるだけ簡単に導出することです。
具体的にはDucksパターンを用いた設計でActionCreatorからActionTypeやActionの型を別途定義することなく実際のActionCreatorの定義からActionを導出するというものです。
別途定義するというのは
Usage With TypeScript · Redux
ここに書いてあるようなActionTypeやActionの型を定義することをさします。
通常のReduxで使う同期ActionとReduxThunkで使う非同期Actionの2つに分けて導出していきます。
同期Action
const setUser = (user: User) => ({ type: 'SET_USER', payload: { user }})
const deleteUser = () => ({ type: 'DELETE_USER' })
const setUser: (user: User) => {
type: string;
payload: {
user: User;
};
}
同期Actionというのはこういうやつですね。多分よくあるActionを返すActionCreatorの定義だと思います。これだとtypeはstringならなんでもいいという型になってしまっています。
まずこれのtypeを定数扱いにするために as const
をtypeに付けます。
const setUser = (user: User) => ({ type: 'SET_USER' as const, payload: { user }})
const deleteUser = () => ({ type: 'DELETE_USER' as const })
次にこれらのそれぞれのActionCreatorの型を導出します。typeofというキーワードで型を導出できるのでそれを用います。
const setUser = (user: User) => ({ type: 'SET_USER', payload: { user }})
const deleteUser = () => ({ type: 'DELETE_USER' })
type SetUserType = typeof setUser
type DeleteUserType = typeof deleteUser
こうすると例えばSetUserTypeは
type SetUserType = (user: User) => {
type: 'SET_USER';
payload: {
user: User;
};
}
このような型として導出されます。
次にこれはActionを作る関数の型なのでActionの型をこれから導出します。ReturnTypeというGenericsの型があるのでそれを使います。これは関数の型定義からReturnされる型を導出してくれます。
type SetUserType = typeof setUser
type DeleteUserType = typeof deleteUser
type SetUserActionType = ReturnType<SetUserType>
type DeleteUserActionType = ReturnType<DeleteUserType>
これでSetUserActionTypeは
type SetUserActionType = {
type: 'SET_USER';
payload: {
user: User;
};
}
という型として導出されます。
これを1つにまとめるとすると
type Action = SetUserActionType | DeleteUserActionType
この型は
type Action = {
type: "DELETE_USER";
} | {
type: "SET_USER";
payload: {
user: User;
};
}
こういう型として導出されます。
毎回これ全部書くのはもちろん面倒なので
type Action = ReturnType<typeof setUser | typeof deleteUser>
これで済みます。
ここまででとりあえずActionの型は導出できましたが、この方法だとActionCreatorが増えるたびにActionの型にも手を加える必要があります。ActionCreatorに追加されたらActionも自動で導出されるようになるのが理想ですね。
まずはそれぞれのActionCretorを1つのオブジェクトとして定義します。
const ActionCreator = {
setUser: (user: User) => ({ type: 'SET_USER' as const, payload: { user }}),
deleteUser: () => ({ type: 'DELETE_USER' as const })
}
このActionCretorからsetUserの型を導出するには
type SetUserType = typeof ActionCreator['setUser']
このように書けます。
すべてのActionCreatorのKeyに対する型の導出はkeyofを使って
type ActionCreatorType = typeof ActionCreator[keyof typeof ActionCreator]
と書けます。
あとはこれを先程の定義と混ぜると
type Action = ReturnType<typeof ActionCreator[keyof typeof ActionCreator]>
こうすることでActionCreatorに追加するとActionの型定義がちゃんと導出されます。
const ActionCreator = {
setUser: (user: User) => ({ type: 'SET_USER' as const, payload: { user }}),
deleteUser: () => ({ type: 'DELETE_USER' as const }),
setHoge: (fuga: string) => ({ type: "SET_HOGE" as const, payload: { fuga }})
}
type Action = ReturnType<typeof ActionCreator[keyof typeof ActionCreator]>
非同期Action
ここではRedux-Thunkを使った非同期処理時の型導出を行います。
Redux-Thunkを使うと関数をdispatchの引数として渡すことができます。その関数の中でdispatchされるアクションの導出が目的となります。今回は例としてユーザ情報を取得するfetchUserという非同期Actionを考えます。
const fetchUser = (id: string) => async (dispatch, getState) => {
dispatch({ type: 'FETCH_USER#START' })
try {
const { data } = await axios.get<User>(`/users/${id}`)
dispatch({ type: 'FETCH_USER#SUCCESS', result: { user: data } })
} catch(e) {
dispatch({ type: 'FETCH_USER#FAILE', error: { errroMessage: "Error" } })
}
}
dipatchされるActionは3つです。
- { type: 'FETCH_USER#START' } 非同期アクションの開始
- { type: 'FETCH_USER#SUCCESS', result: { user: data } } 非同期アクション成功
- { type: 'FETCH_USER#FAILE', error: { errroMessage: "Error" } } 非同期アクション失敗
ここで、ReduxThunkに定義されているThunkActionという非同期アクションの型定義を使います。
export type ThunkAction<R, S, E, A extends Action> = (
dispatch: ThunkDispatch<S, E, A>,
getState: () => S,
extraArgument: E
) => R;
export interface ThunkDispatch<S, E, A extends Action> {
<T extends A>(action: T): T;
<R>(asyncAction: ThunkAction<R, S, E, A>): R;
}
これを使うと先程のfetchUserは以下のように書けます。
const fetchUser = (id: string): ThunkAction<void, Stores, {},
{ type: 'FETCH_USER#START' } |
{ type: 'FETCH_USER#SUCCESS', result: { user: User } } |
{ type: 'FETCH_USER#FAILE', error: { errroMessage: string } }
> => async (dispatch, getState) => {
dispatch({ type: 'FETCH_USER#START' })
try {
const { data } = await axios.get<User>(`/users/${id}`)
dispatch({ type: 'FETCH_USER#SUCCESS', result: { user: data } })
} catch(e) {
dispatch({ type: 'FETCH_USER#FAILE', error: { errroMessage: "Error" } })
}
}
次にこの定義からdispatchされる型を導出します。これを導出するにはinferというキーワードを使って型をキャプチャします。
type AsyncActionType<T> = T extends ThunkAction<any, any, any, infer R> ? R : never
これはAsyncActionTypeのGenerics TがThunkActionを継承している場合、ThunkActionの第4引数の型をinferによってRでキャプチャします。これによりキャプチャされたRがAsyncActionTypeになるという定義です。これを使うとfetchUserがdispatchするActionは以下のように導出できます。
type FetchUserActionTypes = AsyncActionType<typeof ReturnType<typeof fetchUser>>
type FetchUserActionTypes = {
type: "FETCH_USER#START";
} | {
type: "FETCH_USER#SUCCESS";
result: {
user: User;
};
} | {
type: "FETCH_USER#FAILE";
error: {
errroMessage: string;
};
}
あとは同期Actionと同じようにAsyncActionCreatorというオブジェクトを作ってAsyncActionCreatorに追加するだけで自動で導出がされるようにします。
const AsyncActionCreator = {
fetchUser: (id: string): ThunkAction<void, Stores, {},
{ type: 'FETCH_USER#START' } |
{ type: 'FETCH_USER#SUCCESS', result: { user: User } } |
{ type: 'FETCH_USER#FAILE', error: { errroMessage: string } }
> => async (dispatch, getState) => {
dispatch({ type: 'FETCH_USER#START' })
try {
const { data } = await axios.get<User>(`/users/${id}`)
dispatch({ type: 'FETCH_USER#SUCCESS', result: { user: data } })
} catch(e) {
dispatch({ type: 'FETCH_USER#FAILE', error: { errroMessage: "Error" } })
}
}
}
type AsyncActionType<T> = T extends ThunkAction<any, any, any, infer R> ? R : never
type Action = AsyncActionType<ReturnType<typeof AsyncActionCreator[keyof typeof AsyncActionCreator]>>
まとめ
最後に同期Actionと非同期Actionをまとめると
const ActionCreator = {
setUser: (user: User) => ({ type: 'SET_USER' as const, payload: { user } }),
deleteUser: () => ({ type: 'DELETE_USER' as const }),
setHoge: (fuga: string) => ({ type: 'SET_HOGE' as const, payload: { fuga } })
}
const AsyncActionCreator = {
fetchUser: (
id: string
): ThunkAction<
void,
Stores,
{},
| { type: 'FETCH_USER#START' }
| { type: 'FETCH_USER#SUCCESS'; result: { user: User } }
| { type: 'FETCH_USER#FAILE'; error: { errroMessage: string } }
> => async (dispatch, getState) => {
dispatch({ type: 'FETCH_USER#START' })
try {
const { data } = await axios.get<User>(`/users/${id}`)
dispatch({ type: 'FETCH_USER#SUCCESS', result: { user: data } })
} catch (e) {
dispatch({ type: 'FETCH_USER#FAILE', error: { errroMessage: 'Error' } })
}
}
}
type AsyncActionType<T> = T extends ThunkAction<any, any, any, infer R> ? R : never
type Action =
| ReturnType<typeof ActionCreator[keyof typeof ActionCreator]>
| AsyncActionType<ReturnType<typeof AsyncActionCreator[keyof typeof AsyncActionCreator]>>