TS化の一例を挙げます。あくまで一例なので参考に留めていただき、実際のコードを書くときは適宜修正してご自身好みの書き方や設計にフィットさせてください。
まず、ユーザーの型を定義します。
models.ts
typescript
1export interface User {
2 id: number
3 name: string
4 email: string
5}
上記は質問にあるAPIレスポンスに含まれる1個のユーザーのプロパティを端折ったものです。実際はレスポンスに含まれる全プロパティに合わせて interface User
を記述します。この User
はAPI、リデューサー、Reactコンポーネントのいずれからもimportされ得るものです。
- 補足:バックエンドがRailsのようなので上記のファイル名をとりあえず
models.ts
にしておくことについて違和感はそれほど無いと思いますが、人によっては一言モノ申したくなるかもしれません。
reducers/users.ts
constants が質問になかったので、REQUEST_STATE
は以下のようなオブジェクトを想定しました。
typescript
1const REQUEST_STATE = {
2 INITIAL: 0,
3 LOADING: 1,
4 OK: 2,
5 NG: 3,
6}
この前提で、reducers/users.ts
の一例としては以下のようなものになるかと思います。なお以下ではユーザー一覧取得が失敗したときのアクションタイプFETCH_FAILURE
を追加しています。
typescript
1import { REQUEST_STATE } from "../constants";
2import { User } from "../models";
3
4interface State {
5 fetchState: number
6 usersList: User[]
7 error: Error | null
8}
9
10interface Action {
11 type: string
12 payload?: {
13 users: User[]
14 }
15 error?: Error
16}
17
18export const initialState: State = {
19 fetchState: REQUEST_STATE.INITIAL,
20 usersList: [],
21 error: null,
22};
23
24export const usersReducer = (state: State = initialState, action: Action): State => {
25 switch (action.type) {
26 case "FETCHING":
27 return {
28 ...state,
29 fetchState: REQUEST_STATE.LOADING,
30 error: null,
31 };
32 case "FETCH_SUCCESS":
33 return {
34 ...state,
35 fetchState: REQUEST_STATE.OK,
36 usersList: action.payload?.users || [],
37 };
38 case "FETCH_FAILURE":
39 return {
40 ...state,
41 fetchState: REQUEST_STATE.NG,
42 error: action.error || null
43 };
44 default:
45 return state;
46 }
47}
48
apis/users.ts
axios.get
の呼び出しシグニチャに対するジェネリクスは以下のようにするとよいでしょう。
typescript
1import axios from "axios";
2import { usersIndex } from "../urls";
3import { User } from "../models";
4
5interface GetUsersResponse {
6 users: User[];
7}
8
9export const fetchUsers = async () => {
10 try {
11 const res = await axios.get<GetUsersResponse>(usersIndex);
12 return res.data;
13 } catch (e) {
14 console.error(e);
15 throw e;
16 }
17}
18
components/Users_index.tsx
上記のようにしておくと Users_index.tsx は以下のように書いて、Typescriptのエラーは発生せずに、
.then(data =>
のところの data
は GetUsersResponse
に推論され、
state.usersList.map(user =>
の user
は User
に推論される
ものと思います。
tsx
1import React, { useEffect, useReducer } from "react";
2import { fetchUsers } from "../apis/users";
3import { initialState, usersReducer } from "../reducers/users";
4
5export const Users = () => {
6
7 const [state, dispatch] = useReducer(usersReducer, initialState);
8
9 useEffect(() => {
10 dispatch({ type: "FETCHING" });
11 fetchUsers()
12 .then(data =>
13 dispatch({
14 type: "FETCH_SUCCESS",
15 payload: {
16 users: data.users
17 }
18 })
19 ).catch(e =>
20 dispatch({
21 type: "FETCH_ERROR",
22 error: e
23 })
24 )
25 }, [])
26 return (
27 <>
28 <p>ユーザー一覧ページです</p>
29 {
30 state.usersList.map(user =>
31 <div key={user.id}>
32 {user.name}
33 </div>
34 )
35 }
36 </>
37 )
38};
39
追記
上記の回答コードに対する修正を追記しておきます。
- fetchState の取り得る値の型エイリアス
RequestState
を追加
- Action の typeプロパティが取り得る値の型エイリアス
ActionType
を追加
- Usersコンポーネントで fetchState の値ごとに表示内容を振り分け
src/constants.ts
以下に差し替え
typescript
1const REQUEST_STATE = ['INITIAL', 'LOADING', 'OK', 'NG' ] as const;
2
3type RequestState = typeof REQUEST_STATE[number];
4
5export type { RequestState };
6
7
src/reducers/users.ts
diff
1-import { REQUEST_STATE } from "../constants";
2+import { RequestState } from "../constants";
3 import { User } from "../models";
4
5 interface State {
6- fetchState: number
7+ fetchState: RequestState
8 usersList: User[]
9 error: Error | null
10 }
11
12+const actionTypes = ["FETCHING", "FETCH_SUCCESS", "FETCH_FAILURE"] as const;
13+type ActionType = typeof actionTypes[number];
14+
15 interface Action {
16- type: string
17+ type: ActionType
18 payload?: {
19 users: User[]
20 }
diff
1 export const initialState: State = {
2- fetchState: REQUEST_STATE.INITIAL,
3+ fetchState: "INITIAL",
4 usersList: [],
5 error: null,
6 };
diff
1 case "FETCHING":
2 return {
3 ...state,
4- fetchState: REQUEST_STATE.LOADING,
5+ fetchState: "LOADING",
6 error: null,
7 };
8 case "FETCH_SUCCESS":
9 return {
10 ...state,
11- fetchState: REQUEST_STATE.OK,
12+ fetchState: "OK",
13 usersList: action.payload?.users || [],
14 };
15 case "FETCH_FAILURE":
16 return {
17 ...state,
18- fetchState: REQUEST_STATE.NG,
19- error: action.error || null
20+ fetchState: "NG",
21+ error: action.error || new Error("unknown error")
22 };
23 default:
24 return state;
src/components/Users_index.tsx
diff
1 export const Users = () => {
2
3- const [state, dispatch] = useReducer(usersReducer, initialState);
4+ const [{ fetchState, usersList, error }, dispatch] = useReducer(usersReducer, initialState);
5
6 useEffect(() => {
7 dispatch({ type: "FETCHING" });
8
diff
1 users: data.users
2 }
3 })
4- ).catch(e =>
5- dispatch({
6- type: "FETCH_ERROR",
7- error: e
8- })
9- )
10- }, [])
11+ )
12+ .catch(e =>
13+ dispatch({
14+ type: "FETCH_FAILURE",
15+ error: e
16+ })
17+ )
18+ }, []);
19+
20+ if (fetchState === "LOADING") {
21+ return <div>データ取得中...</div>;
22+ }
23+
24 return (
25 <>
26 <p>ユーザー一覧ページです</p>
27- {
28- state.usersList.map(user =>
29- <div key={user.id}>
30- {user.name}
31- </div>
32- )
33+ {error
34+ ? <div>エラーが発生しました。原因: {error.message} </div>
35+ : usersList.map(({ id, name }) => <div key={id}>{name}</div>)
36 }
37 </>
38 )