「Next.jsでつくるフルスタックアプリwith TypeScript 後編」の感想・備忘録1の続きitemディレクトリのTypeScript化
pages/user/register.jsのTypeScript化
- mv pages/item/create.js pages/item/create.tsx
- 修正内容はpages/user/register.tsxと全く同じ。
import type { NextPage } from "next"; // 追加
import { useState } from 'react';
import useAuth from "../../utils/useAuth";
const CreateItem: NextPage = () => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [price, setPrice] = useState('');
// : React.FormEvent<HTMLFormElement>を追加
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const response = await fetch('http://localhost:3000/api/item/create', {
method: 'POST',
headers: {
'Accrpt': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({
title: title,
description: description,
price: price,
})
});
const json = await response.json();
alert(json.msg);
} catch (err) {
alert('登録失敗');
}
};
const email = useAuth();
return (
<form onSubmit={handleSubmit}>
<h1>アイテム作成</h1>
<p>title: <input type="text" name="title" value={title} onChange={e=>setTitle(e.target.value)} required/></p>
<p>description: <input type="text" name="description" value={description} onChange={e=>setDescription(e.target.value)}/></p>
<p>price: <input type="text" name="price" value={price} onChange={e=>setPrice(e.target.value)}/></p>
<p><button type="submit">投稿</button></p>
</form>
)
};
export default CreateItem;
pages/item/[id].jsのTypeScript化
- mv pages/item/[id].js pages/item/[id].tsx
- propsに含まれるitemの型定義が必要なので、utils/types.tsにReadSingleDataTypeを追加する。
- propsに型を指定するのではなく、ジェネリックを使って関数コンポーネントの型をNextPage<ReadSingleDataType>とする。
- getServerSideProps関数の型はジェネリックを使ってGetServerSideProps<ReadSingleDataType>とする。
(GetServerSidePropsはnextからのimport)
import type { NextPage, GetServerSideProps } from "next"; // 追加
import Link from "next/link";
import { ReadSingleDataType } from '../../utils/types' // 追加
// : NextPage<ReadSingleDataType>を追加
const SingleItem: NextPage<ReadSingleDataType> = (props) => {
return (
<>
<h1>title: {props.item.title}</h1>
<h2>descripton: {props.item.description}</h2>
<h3>price: {props.item.price}</h3>
<h3>email: {props.item.email}</h3>
<Link href={`/item/update/${props.item._id}`} style={{marginRight: '10px'}}>編集</Link>
<Link href={`/item/delete/${props.item._id}`} style={{marginRight: '10px'}}>削除</Link>
<Link href="/item">一覧へ戻る</Link>
</>
)
};
export default SingleItem;
// : GetServerSideProps<ReadSingleDataType>を追加
export const getServerSideProps: GetServerSideProps<ReadSingleDataType> = async (context) => {
const response = await fetch(`http://localhost:3000/api/item/${context.query.id}`);
const item = await response.json();
return {
props: item
}
};
import type {NextApiRequest} from "next";
import {Types} from "mongoose";
// schemaModel.ts
export interface ItemDataType {
title: string,
image: string,
price: string,
description: string,
email: string
}
export interface UserDataType {
name: string,
email: string,
password: string,
}
// auth.ts
export interface DecodedType {
email: string
}
export interface ExtendedNextApiRequestAuth extends NextApiRequest {
headers: {
authorization: string
},
body: {
email: string
}
}
// register.ts, login.ts
export interface ExtendedNextApiRequestUser extends NextApiRequest {
body: UserDataType
}
// login.ts
export interface SavedUserDataType extends UserDataType {
_id: Types.ObjectId
}
// readall.ts, [id].ts, update[id].ts, delete/[id].ts
export interface SavedItemDataType extends ItemDataType {
_id: Types.ObjectId
}
// readall.ts
export interface ResReadAllType {
msg: string,
allItems?: SavedItemDataType[]
}
// create.ts
export interface ExtendedNextApiRequestItem extends NextApiRequest {
body: ItemDataType
}
// [id].ts
export interface ResReadSingleType {
msg: string,
item?: SavedItemDataType
}
// 追加
// Frontend
// [id].tsx, update[id].tsx, delete/[id].tsx
export interface ReadSingleDataType {
item: {
_id: string,
title: string,
image: string,
price: string,
description: string,
email: string
}
}
// common
export interface ResMsgType {
msg: string,
token?: string
}
pages/item/update/[id].jsのTypeScript化
- mv pages/item/update/[id].js pages/item/update/[id].tsx
- returnがif文の中にしかないので、else文を追加。
- それ以外の修正内容はpages/item/[id].tsxと全く同じ。
import type { NextPage, GetServerSideProps } from "next"; // 追加
import { useState } from 'react';
import useAuth from "../../../utils/useAuth";
import { ReadSingleDataType } from '../../../utils/types' // 追加
// : NextPage<ReadSingleDataType>を追加
const UpdateItem: NextPage<ReadSingleDataType> = (props) => {
const [title, setTitle] = useState(props.item.title);
const [description, setDescription] = useState(props.item.description);
const [price, setPrice] = useState(props.item.price);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const response = await fetch(`http://localhost:3000/api/item/update/${props.item._id}`, {
method: 'POST',
headers: {
'Accrpt': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({
title: title,
description: description,
price: price,
})
});
const json = await response.json();
alert(json.msg);
} catch (err) {
alert('更新失敗');
}
};
const email = useAuth();
if (email === props.item.email) {
return (
<form onSubmit={handleSubmit}>
<h1>アイテム作成</h1>
<p>title: <input type="text" name="title" value={title} onChange={e => setTitle(e.target.value)} required/></p>
<p>description: <input type="text" name="description" value={description}
onChange={e => setDescription(e.target.value)}/></p>
<p>price: <input type="text" name="price" value={price} onChange={e => setPrice(e.target.value)}/></p>
<p>
<button type="submit">保存</button>
</p>
</form>
);
} else { // 追加
return <></>
}
};
export default UpdateItem;
// : GetServerSideProps<ReadSingleDataType>を追加
export const getServerSideProps: GetServerSideProps<ReadSingleDataType> = async (context) => {
const response = await fetch(`http://localhost:3000/api/item/${context.query.id}`);
const item = await response.json();
return {
props: item
}
};
その他のファイルのTypeScript化
utils/useAuth.jsのTypeScript化
- mv utils/useAuth.js utils/useAuth.tsx
- tokenはif文でチェックしているため、verifyの時点では必ず値が存在する。
- 非nullアサーション演算子!を使ってtoken!とすると、nullあるいはundefinedにはならないことをTypeScriptに教えることができる。
jwt.verify(token as string, 'hogehogehoge');
のようにstringであるとしてもよい。 - jwt.verufyの戻り値の型はstring | jwt.JwtPayloadであるため、型アサーションを使って変数の型を上書きする。
(バックエンドのutils/auth.tsと同じ)
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import jwt from "jsonwebtoken";
import { DecodedType } from './types'; // 追加
const useAuth = () => {
const [email, setEmail] = useState('');
const router = useRouter();
useEffect(() => {
const token = localStorage.getItem('token');
if (!token) {
router.push('/user/login');
}
try {
const decoded = jwt.verify(token!, 'hogehogehoge'); // token!に修正
setEmail((decoded as DecodedType).email); // (decoded as DecodedType)に修正
} catch (err) {
router.push('/user/login');
}
}, [router]);
return email;
};
export default useAuth;
pages/item/index.jsのTypeScript化
- mv pages/item/index.js pages/item/index.tsx
import type { NextPage, GetServerSideProps } from "next"; // 追加
import Link from "next/link";
import { ReadAllDataType } from '../../utils/types' // 追加
// : NextPage<ReadAllDataType>を追加
const ItemList: NextPage<ReadAllDataType> = (props) => {
return (
<>
{ props.allItems.map(item=> {
return <p key={item._id}><Link href={`item/${item._id}`}>{item.title}</Link>: {item.description.substring(0, 20)}: {item.price}: {item.email}</p>
})}
</>
)
};
export default ItemList;
// : GetServerSideProps<ReadAllDataType>を追加
export const getServerSideProps: GetServerSideProps<ReadAllDataType> = async () => {
const response = await fetch('http://localhost:3000/api/item/readall');
const allItems = await response.json();
return {
props: allItems
}
};
コメント