TypeScript、ネストする型定義とFE/BEでの使い方
前提
- 研修を終えて配属一週間目のひよこです
- 研修で触れた言語やフレームワーク
- HTML, CSS
- JavaScript
- React
- TypeScript
- python
- Java
- kotlin
- SpringBoot
- 研修で触れた言語やフレームワーク
- 諸々触れることはできていても理解は甘々なので、自分でwebアプリを作ってみてます
- TODO管理アプリのフロントとバックを作ります
- 使用する言語など
- フロント
- TypeScript
- React
- バック
- TypeScript
- Firebase Function
- フロント
- プロダクトのコンセプト
- TODO粒度での振り返りを行いやすくすることで、成長のPDCAを加速させる
- 使用する言語など
この記事の目的
自分の定着目的で、現状の理解をメモ程度にアウトプットします。
人に読ませるつもりでは書けてないです。
やりたいこと
- TODO管理アプリにて、タスクカテゴリ配下に複数のタスクを登録(POST)できるようにしたい
- それにまつわる諸々をアウトプットします
やったこと(着手順)
型定義
- まずはTaskGroupとTaskItemそれぞれの完全版の型を定義します
- 今回作るPOSTの場合、IDをリクエストに含められない(firebaseの自動採番)ので、Request用の型も別途用意する必要があります
- 構成として、フロントからもバックからも参照されるので、ルート直下にcommonというフォルダを作り、その中のschemaというフォルダに書きました
- フロントで共通の資材(headerなど)は、フロントのコードが集まってるsrc直下にcommonというフォルダを作って書いていくつもりです。
TaskItem
タスクにステータスや優先度などのフラグをつけたり、振り返りや完了日をつけたりできるようにしたい。
振り返りを別DBで持つか迷いましたが、なんかIDで紐付けるのが大変になりそうなのでTaskItemに全部持たせることにしました。
export type TaskItem = {
taskItemId: string;
taskItemCreateAt: Date;
taskItemDeadline: Date;
taskItemTitle: string;
taskItemEstimateTimes?: number | null;
taskItemMemo?: string | null;
taskItemFlag?: "未着手" | "進行中" | "ペンド" | null;
taskItemPriority?: "未判断" | "優先" | "非優先" | null;
taskItemDoneFlag: boolean;
taskItemDoneDate?: Date | null;
taskItemReflectionGood?: string[] | null;
taskItemReflectionMore?: string[] | null;
}
この型を参照してFEでもBEでも書くことになります。
型エラーが起きたときは大体ここを見返します。
なお、
taskItemFlag?: "未着手" | "進行中" | "ペンド" | null;
select要素で、あらかじめ定義したどれかのvalueになっていて欲しい場合は上記のように書けるみたいです。
「|」って使い道たくさんありますね。
また、入力しなくてもいいのでnullにしました。
TaskGroup
import { TaskItem } from "./TaskItem";
export type TaskGroup = {
taskGroupId: string;
taskGroupName: string; //案件名など
taskGroupItems?: TaskItem[] //案件に紐づく複数タスクの配列
}
taskGroupに複数のタスクを持たせたいので、先ほど定義したTaskItemの配列型で定義しました。
RequestTaskItem
POST時に使う型なので、完了日などPOSTで使わないものは除きました
これが吉と出るかはまだわかっていませんが、PUTの時はまた別の型を定義しようと思ってます
export type RequestTaskItem = {
taskItemDeadline?: Date | null;
taskItemTitle: string;
taskItemEstimateTimes?: number | null;
taskItemMemo?: string | null;
taskItemFlag?: "未着手" | "進行中" | "ペンド";
taskItemPriority?: "未判断" | "優先" | "非優先";
}
またIDや作成日もPOSTで登録されるものですが、フロントからのリクエストに含まれるものではないので必然的に除いています。
RequestTaskGroup
POST時に使う型なので、完了日などPOSTで使わないものは除きました
これが吉と出るかはまだわかっていませんが、PUTの時はまた別の型を定義しようと思ってます
import { TaskItem } from "./TaskItem";
export type TaskGroup = {
taskGroupId: string;
taskGroupName: string;
taskGroupItems?: TaskItem[]
}
そういえば、export constで書いた関数をインポートする場合は、{}で囲まないと「既定のエクスポートがありません」というエラーになります。
パス指定があってるのに上記エラーが起きる場合は確認しましょう。
Firebase Function
- functions/src/index.tsに、一旦全部まとめて記載
設定的な部分
エラーに苦しむ自分にAIが差し伸べた以下2つは、呪文のように貼りました。
const corsHandler = cors({ origin: true });
admin.initializeApp();
CORSエラーが出ている時は上記1つ目が大事。
でもadminのやつが本当に重要で、これを書いている位置がどこかの関数内だった時はデプロイエラーがでまくりました。
admin.initializeApp();はファイルの冒頭で書くべし。
POSTのコード
- 役割
- firebaseにTaskGroupと、その配下のTaskItem[]をPOSTする
- やらなきゃいけないこと
- CORS処理でアクセスの許可
- TaskItemは複数登録可能なので、mapにする
- firebaseにpostする
- それぞれのIDをFirebaseのIDからとってくる
- レスポンスやエラーをreturnする
- 実際のコード
exports.createTaskGroup = myFunction.onRequest((request, response) =>{
corsHandler(request, response, async () => {
try {
const requestNewTaskGroup = request.body;
const newTaskGroupRef = admin.firestore().collection('taskGroup').doc();
const taskGroupId = newTaskGroupRef.id;
const taskGroupItems = requestNewTaskGroup.taskGroupItems.map((item: TaskItem) => {
const newTaskItemRef = admin.firestore().collection('taskItem').doc();
const taskItemId = newTaskItemRef.id;
const taskItemCreateAt = new Date();
return {
taskItemId,
taskItemCreateAt,
taskItemDeadline: new Date(item.taskItemDeadline),
taskItemTitle: item.taskItemTitle,
taskItemEstimateTimes: item.taskItemEstimateTimes,
taskItemMemo: item.taskItemMemo,
taskItemFlag: item.taskItemFlag,
taskItemPriority: item.taskItemPriority,
taskItemDoneFlag: false,
taskItemDoneDate: null,
taskItemReflectionGood: [],
taskItemReflectionMore: []
};
});
const newTaskGroup: TaskGroup = {
taskGroupId,
taskGroupName: requestNewTaskGroup.taskGroupName,
taskGroupItems: taskGroupItems,
};
await admin.firestore().collection('taskGroup').doc(newTaskGroup.taskGroupId).set(newTaskGroup);
return response.status(200).json(newTaskGroup);
} catch (error) {
console.error('functionのcreateTaskGroupでキャッチしたエラー', error);
return response.status(500).json({error: 'Internal Server Error'});
}
})
});
-
exports
はFirebaseが提供している関数で、とりあえずこういう書き方にするとFunctionに登録できる -
onRequest
のrequest
やresponse
もFirebaseが提供している関数で、リクエストの内容をオブジェクトとして持ったりできるイメージ- アロー関数にしてるからrequestとかを引数として扱えるようになってるというイメージ
-
corsHandler
は、リクエストが許可されているかをチェックするミドルウェアらしい-
request
とresponse
を引数としてして受け取りつつ、CORSでのチェックが完了したらasync
内の処理を実行する感じ - thisとかないからアローにする意味はそんなにないけど書きやすいからアロー
-
-
try
内に処理を書いていく- フロントが送った内容を
request.body
として取得し、requestNewTaskGroup
という関数に詰め込む - firebaseのDBに新しく場所(
collection
とdoc
)を確保した処理の結果をnewTaskGroupRef
として定義 -
newTaskGroupRef
が持っているidをtaskGroupId
に代入する
- フロントが送った内容を
-
taskGroup
配下に複数入力されるかもしれない各タスクをmap化したtaskGroupItems
を作る-
TaskGroup
の型でtaskGroupItems
を定義していたので、request.body
にはtaskGroupItems
という配列があるため、一旦その配列を取ってくる - その
taskGroupItems
をmap化し、配列の一つ一つをitem
という変数に代入。下ごしらえ完了。 -
taskItem
のidをfirebaseから取ってくるために、taskItem
というcollectionとdocをnewTaskItemRef
として作成 -
newTaskItemRef
のidをtaskItemId
として定義 - 別途タスクの作成日を登録したいので、
taskItemCreateAt
としてnew Date()
を記載- ()に何も指定しないとタイムスタンプが取れるっぽい
-
return
内に、TaskItem
として持たせたいカラムを全部記載-
RequestTaskItem
と迷ったけど、POSTでDB作りたいので一旦null含めて全カラムを書いた
-
-
- そうして得られた
taskGroupItems
の配列を、POSTしたいnewTaskGroup
に詰め込む-
newTaskGroup
も全カラムを持ったTaskGroup型
-
TaskGroupId
は上の方で取ってきてる -
TaskGroupName
はフロントで入力された内容 -
taskGroupItems
として、さっき作ったtaskGroupItems
を入れる
-
- firebaseにPOSTする
- collectionとしてさっき作った
taskGroup
を指定 - どのdocにPOSTするか、
taskGroupId
で指定- ちなみにcollectionがDBで、docがレコードというイメージ
-
set
内にPOSTしたい内容を書くとPOSTできるので、set(newTaskGroup)
と書く
- collectionとしてさっき作った
- 正常に実行できた場合のresponseをreturnする
-
status(200)
みたいに200にするのはHTTPの慣習的な感じで、別に200以外でも動くけど200にするもの - json形式で
newTaskGroup
を返すのも忘れずに。
-
- catch処理を書く
- まあerrorを出力する感じ
- どの段階でerrorが起きたかわからないと大変だから日本語で詳しめに書いてみてるけど、効力があるかはまだ感じられていない。errorをthrowするapiをFEに書くからあんま意味ない気がしてる。
フロントのコード
- 役割
- POSTするカテゴリ名やタスク詳細を入力する
- その値をapiに送る
- やりたいこと
- 「+」ボタンを押すとタスク入力欄が増えて、いくつも入力できるようにしたい
import React, { useState } from "react";
import { RequestTaskGroup } from "../../../../common/schema/RequestTaskGroup";
import { RequestTaskItem } from "../../../../common/schema/RequestTaskItem";
import { createTaskGroup } from "./api";
export const CreateTask: React.FC = () => {
const [taskGroupName, setTaskGroupName] = useState<string> ('');
const [taskItems, setTaskItems] = useState<RequestTaskItem[]>([]);
const handleAddTaskItem = () => {
setTaskItems([...taskItems, {
taskItemTitle: '',
taskItemDeadline: null,
taskItemEstimateTimes: null,
taskItemMemo: null,
taskItemFlag: '未着手',
taskItemPriority: '未判断',
}]);
};
//keyofが存在しない値の可能性を排除するために以下で型指定
const handleTaskItemChange = <K extends keyof RequestTaskItem>(index: number, field: K, value: RequestTaskItem[K])=> {
const newTaskItems = [...taskItems];//taskItems(RequestTaskItem[]型)の配列としてnewTaskItemsを宣言
newTaskItems[index][field] = value;//newTaskItemsのindex番目のfieldキーに引数のvalueを代入
setTaskItems(newTaskItems);
};
const handleCreateTaskGroup = async () => {
const newTaskGroup: RequestTaskGroup = {
taskGroupName,
taskGroupItems: taskItems,
};
try {
const createdTaskGroup = await createTaskGroup(newTaskGroup); // createTaskGroup関数はAPI呼び出しを行う
console.log('Task Group created:', createdTaskGroup);
} catch (error) {
console.error('Failed to create task group:', error);
}
};
return (
<div>
<div>
<input
type="text"
value={taskGroupName}
onChange={(e) => setTaskGroupName(e.target.value)}
placeholder="タスクグループ名を入力"
/>
</div>
{taskItems.map((taskItem, index) => (
<div key={index}>
<input
type="text"
value={taskItem.taskItemTitle}
onChange={(e) => handleTaskItemChange(index, 'taskItemTitle', e.target.value)}
placeholder="タスクタイトルを入力"
/>
<input
type="number"
value={taskItem.taskItemEstimateTimes !== null ? taskItem.taskItemEstimateTimes : ''}
onChange={(e) => handleTaskItemChange(index, 'taskItemEstimateTimes', e.target.value ? Number(e.target.value) : null)}
placeholder="予想分数を入力"
/>
<input
type="date"
value={taskItem.taskItemDeadline ? taskItem.taskItemDeadline.toISOString().substr(0, 10) : ''}
onChange={(e) => handleTaskItemChange(index, 'taskItemDeadline', e.target.value ? new Date(e.target.value) : null)}
/>
<input
type="text"
value={taskItem.taskItemMemo !== null ? taskItem.taskItemMemo : ''}
onChange={(e) => handleTaskItemChange(index, 'taskItemMemo', e.target.value)}
placeholder="メモを入力"
/>
<select value={taskItem.taskItemFlag || '未着手'}
onChange={(e) => handleTaskItemChange(index, 'taskItemFlag', e.target.value as "未着手" | "進行中" | "ペンド")}>
<option value="未着手">未着手</option>
<option value="進行中">進行中</option>
<option value="ペンド">ペンド</option>
</select>
<select value={taskItem.taskItemPriority || '未判断'}
onChange={(e) => handleTaskItemChange(index, 'taskItemPriority', e.target.value as "未判断" | "優先" | "非優先")}>
<option value="未判断">未判断</option>
<option value="優先">優先</option>
<option value="非優先">非優先</option>
</select>
</div>
))}
<button onClick={handleAddTaskItem}>+ タスクを追加</button>
<button onClick={handleCreateTaskGroup}>タスクグループを登録</button>
</div>
);
}
特筆したいこと
- タスクを簡単に複数入力できるようにする
- ボタンのonClickで
handleAddTaskItem
が起動- taskItemsを
...
で配列として使えるようにする- teskItemsはuseStateで初期値[]の配列
RequestTaskItem[]
型として定義しておく
- teskItemsはuseStateで初期値[]の配列
- 起動するとuseStateに初期値が入力されるようにする
- taskItemsを
- それぞれのinput要素で
handleTaskItemChange
を起動させる-
taskItems
の配列をmapとして表示していて、そのmapの何番目かをreturn内でindex
として持てる。この「何個か存在するかわからない個数を管理する」ためのmapとindexは結構使いそうな気がするので覚える。- indexで区別しないと、一つのinputに入力した内容が他のタスクにも反映されちゃったりする
-
handleTaskItemChange
の第一引数にindexを、第二引数にその変更した値のkey名を、第三引数に入力されたvalueを送る - このkey名が、一応型定義されていないkey名も入力できるため型エラーが出る
- よってkeyが
RequestTaskItem
の値だと明示して使う
-
- ボタンのonClickで
- 型エラー
-
handleTaskItemChange
とかのselect要素が、型定義外の値を入力されても検知できないようになってしまう-
e.target.value as "未着手" | "進行中" | "ペンド"
みたいに記載することで防げる - 多分もっといい書き方ある気がする
-
-
- mapの使い方改めて
-
taskItems
が繰り返し表示したい配列要素 -
taskItems.map((taskItem, index) => (
みたいにして、配列の一つ一つの要素をtaskItem
として持ち、それがmap内の何番目かをindex
として持つ - 繰り返し表示したい部分を
<div key={index}></div>
として囲う- indexで区別する
- 表示要素を追加する
-
handleAddTaskItem
が呼び出されるとtaskItems
の中身が追加される -
taskItems
がmapとして繰り返し表示されるので、中身が追加されたらinput要素もちゃんと増える
-
-
- apiにリクエストを送信
-
await createTaskGroup(newTaskGroup)
として、createTaskGroup
というAPIを呼び出す - ここで引き渡す
newTaskGroup
をhandleCreateTaskGroup
内で作成 - この時のリクエスト内容はIDとかがないので、別で定義している
RequestTaskGroup
型で定義
-
フロントとバックの繋ぎこみ(api)
- 役割
- フロント側で受け取った値をバックでapiを定義した関数に送る
- どのapiを起動するかなどを書く
import axios from "axios";
import { RequestTaskGroup } from "../../../../common/schema/RequestTaskGroup";
export async function createTaskGroup(taskGroup: RequestTaskGroup){
const functionUrl = 'ここにFirebase Functions上のURLを書く';
try {
const response = await axios.post<RequestTaskGroup>(functionUrl, taskGroup);
return response.data;
} catch (error) {
console.error('apiでキャッチしたエラー', error);
throw error;
}
}
- 肝は
await axios.post<RequestTaskGroup>(functionUrl, taskGroup)
- axiosは第一引数で対象APIのURLを、第二引数でAPIに送る値を書く
- このためにメソッドで
taskGroup
を受け取る。- さっき
handleCreateTaskGroup
で送ったやつ
- さっき
- 「フロントとバックの繋ぎこみ」ってなんなのかわからない時期長かったけど、接点になってる部分はフロントで書くapi用のコード。
- ここで対象APIを指定して値を送っていて、そのための値の準備としてフロントの他のコードが存在するという感じ。
振り返り
- もっと細かい粒度で書かないと定着には繋がらなそう
- こまめに書いていく
- 1日で1つをちゃんと定着させる、というのが大事な気がしていて、その日に学んだことを何か一個に絞って記事にしていく
- 日によってはいくつもの色んなことを学ぶだろうけど、何かしらピンを打たないとどれも定着せずに終わりそう
- 今回で言うと、ネストする型定義やその型の送り方、mapの使い方などは一定身についた気がする
- 型定義の具体についてはケースバイケースという印象で、そこは今後学んで記事にしていく
Discussion