「Next.jsでつくるフルスタックアプリwith TypeScript 後編」の感想・備忘録2

スポンサーリンク
「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
  }
};

コメント