LoginSignup
0
0

JavaScriptをTypeScriptに置き換えてみた!

Last updated at Posted at 2024-06-30

はじめに

こんにちは!WEBエンジニア転職を目指しているK.Yです!
以下のJavaScriptコードをTypeScriptに置き換えてみました!

React/JavaScriptで実装した記事もあります。

重複している部分は省略しています!

コード

index.tsx

import React from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';

const container = document.getElementById('root');
const root = createRoot(container!); // TypeScriptではnullチェックが必要です

root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);
App.tsx

import './App.css';
import { Routes, Route } from 'react-router-dom';
import PostsList from './PostsList';
import DetailsPage from './DetailsPage';
import InquiryPage from './InquiryPage';

const App: React.FC = () => {
  return (
    <Routes>
      <Route path="/" element={<PostsList />} />
      <Route path="/post/:id" element={<DetailsPage  />} />
      <Route path="/inquiry" element={<InquiryPage />} />
    </Routes>
  );
};

export default App;
PostsList.tsx

import React from 'react';
import { useEffect, useState } from 'react'
import './App.css';
import { Link } from 'react-router-dom';

type ArticleType = {
id: number;
createdAt: string;
categories: string[];
title: string;
content: string;
}

type PostsType = {
  posts: ArticleType[];
}

const PostsList: React.FC = () => {

  const formatDate = (dateString: string) => {
    const date = new Date(dateString);
    const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'numeric', day: 'numeric' };

    return date.toLocaleDateString('ja-JP', options);
  };

  const [posts, setPosts] = useState<PostsType>({ posts: [] })
  const [loading, setLoading] = useState<boolean>(true); // ローディング状態を追加

  useEffect(() => {
    const fetchData = async () => {
      try {

        const response = await fetch("https://1hmfpsvto6.execute-api.ap-northeast-1.amazonaws.com/dev/posts");
        const data = await response.json() as PostsType

        setPosts(data);

      }  finally {
        setLoading(false); // データ取得が完了したらローディングを終了
      }
    };

    fetchData();
  }, []);

  if (loading) {
    return <div>Loading...</div>; // ローディング中の表示
  }

      
  return (

    <div className="App">
      <header className="header-App">
        <Link className="link" to="/">Blog</Link>
        <Link className="link" to="/inquiry">お問い合わせ</Link>
      </header>

      {
        Array.isArray(posts.posts) && posts.posts.map(article => (
          <div key={article.id} className="posts-info">
            <ul className="post-list">
              <li className="post-item">
                <Link to={`/post/${article.id}`}>
                  <div className="date">{formatDate(article.createdAt)}</div>
                  <div className="programming-language">{article.categories.map((category, idx) => (
                    <span key={idx} className="category-box">{category}</span>
                  ))}</div>
                  <div className="title">{article.title}</div>
                  <div className="content" dangerouslySetInnerHTML={{ __html: article.content }}>
                  </div>
                </Link>
              </li>
            </ul>
          </div>
        ))}

    </div>
  );
    }

export default PostsList;
DetailsPage.tsx

import React from 'react';
import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import "./App.css";

type detailsType = {
  id: number;
  createdAt: string;
    thumbnailUrl: any;
  categories: string[];
  title: string;
  content: string;
};

type ApiResponse = {
  post: detailsType;
}

const DetailsPage: React.FC = () => {
  const { id } = useParams();

  const formatDate = (dateString: string): string => {
    const date = new Date(dateString);
    const options: Intl.DateTimeFormatOptions = {
      year: "numeric",
      month: "numeric",
      day: "numeric",
    };
    return date.toLocaleDateString("ja-JP", options);
  };

  const [detailsData, setDetailsData] = useState<detailsType | null>(null);
  const [loading, setLoading] = useState<boolean>(true); // ローディング状態を追加

  useEffect(() => {
    const fetchData = async () => {
      try {

        const response = await fetch(
          `https://1hmfpsvto6.execute-api.ap-northeast-1.amazonaws.com/dev/posts/${id}`
        );
        const result = await response.json() as ApiResponse;

        setDetailsData(result.post);
      } finally {
        setLoading(false); // データ取得が完了したらローディングを終了
      }
    };

    fetchData();
  }, [id]);

  if (loading) {
    return <div>Loading...</div>; // ローディング中の表示
  }

  if (!detailsData) return <div>投稿が見つかりません</div>;

  return (
    <div className="App">
      <header className="header-App">
        <Link to="/" className="link">
          Blog
        </Link>
        <Link to="/inquiry" className="link">
          お問い合わせ
        </Link>
      </header>

      <div style={{ border: "none" }} className="posts-info">
        <ul className="post-list">
          <li key={detailsData.id} className="post-item">
            <div className="img">
              <img src={detailsData.thumbnailUrl} alt="img" />
            </div>
            <div className="date">{formatDate(detailsData.createdAt)}</div>
            <div className="programming-language">
              {detailsData.categories.map((category, idx) => (
                <span key={idx} className="category-box">
                  {category}
                </span>
              ))}
            </div>
            <div className="title">{detailsData.title}</div>
            <div
              style={{ display: "block" }}
              className="content"
              dangerouslySetInnerHTML={{ __html: detailsData.content }}
            ></div>
          </li>
        </ul>
      </div>
    </div>
  );
};

export default DetailsPage;
InquiryPage.tsx

import React from 'react';
import { Link } from "react-router-dom";
import { FormEvent, useState } from "react";
import "./App.css";

type InquiryType = {
  name: string ;
  email: string;
  message: string;
};

type ErrorsType = {
  name?: string;
  email?: string;
  message?: string;
};

const InquiryPage: React.FC = () => {
  const [inquiryData, setInquiryData] = useState<InquiryType>({
    name: "",
    email: "",
    message: "",
  });
  const [errors, setErrors] = useState<ErrorsType>({});
  const [isSubmitting, setIsSubmitting] = useState<boolean>(false);

  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    const { id, value } = e.target;
    setInquiryData((prevData) => ({ ...prevData, [id]: value }));
  };

  const validate = () => {
    const tempErrors: ErrorsType = {};
    if (!inquiryData.name) tempErrors.name = "お名前は必須です。";
    if (!inquiryData.email) tempErrors.email = "メールアドレスは必須です。";
    if (!inquiryData.message) tempErrors.message = "本文は必須です。";
    setErrors(tempErrors);
    return Object.keys(tempErrors).length === 0;
  };

  const handleSubmit = async (e: FormEvent): Promise<void> => {
    e.preventDefault();
    if (!validate()) return;

    setIsSubmitting(true);

    try {
      const response = await fetch(
        "https://1hmfpsvto6.execute-api.ap-northeast-1.amazonaws.com/dev/contacts",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(inquiryData),
        }
      );

      if (!response.ok) throw new Error("Network response was not ok");

      alert("送信しました");
      setInquiryData({ name: "", email: "", message: ""});
      setErrors({});
    } catch (error) {
      console.error("Error submitting form:", error);
    } finally {
      setIsSubmitting(false);
    }
  };

  const handleClear = () => {
    setInquiryData({ name: "", email: "", message: "" });
  };
  return (
    <div className="App">
      <header className="header-App">
        <Link to="/" className="link">
          Blog
        </Link>
        <Link to="/inquiry" className="link">
          お問い合わせ
        </Link>
      </header>

      <div className="inquiry">
        <h1>問合わせフォーム</h1>
        <form id="myForm" onSubmit={handleSubmit}>
          <div className="formItem">
            <label>
              <dl>
                <dt>お名前</dt>
                <div className="text">
                  <dd>
                    <input
                      type="text"
                      id="name"
                      maxLength={30 as number}
                      value={inquiryData.name}
                      onChange={handleChange}
                      disabled={isSubmitting}
                    />
                  </dd>
                  {errors.name && <span>{errors.name}</span>}
                </div>
              </dl>
            </label>

            <div className="label">
              <label>
                <dl>
                  <dt>メールアドレス</dt>
                  <div className="text">
                    <dd>
                      <input
                        type="text"
                        id="email"
                        value={inquiryData.email}
                        onChange={handleChange}
                        disabled={isSubmitting}
                      />
                    </dd>
                    {errors.email && <span>{errors.email}</span>}
                  </div>
                </dl>
              </label>
            </div>
            <div className="label">
              <label>
                <dl>
                  <dt>本文</dt>
                  <div className="text">
                    <dd>
                      <textarea
                        id="message"
                        maxLength={500 as number}
                        value={inquiryData.message}
                        onChange={handleChange}
                        disabled={isSubmitting}
                        rows={10 as number}
                      />
                    </dd>
                    {errors.message && <span>{errors.message}</span>}
                  </div>
                </dl>
              </label>
            </div>
          </div>
          <div className="btn">
            <input type="submit" value="送信" disabled={isSubmitting} />
            <input
              type="reset"
              value="クリア"
              onClick={handleClear}
              disabled={isSubmitting}
            />
          </div>
        </form>
      </div>
    </div>
  );
};

export default InquiryPage;

TypeScript

TypeScriptとは、JavaScirptの代替言語(altJS)の一種。
JavaScirptは性質上、型が厳密に意識したコーディングができません。
ある程度規模の大きなアプリでは、型の曖昧さは、潜在的なバグを生む原因になります。

そうしたJSの弱点を補うのがaltJSの役割。
altJSは、トランスパイラーによって、JSに変更してから実行されます。

index.tsx (Reactアプリケーションのエントリーポイント)

import { createRoot } from 'react-dom/client';

const container = document.getElementById('root');
const root = createRoot(container!); // TypeScriptではnullチェックが必要

container!: TypeScriptの非nullアサーション演算子で、containerがnullではないことを保証します。

App.tsx (アプリケーションのルートコンポーネント)

const App: React.FC = () => {
  return (
    <Routes>
      <Route path="/" element={<PostsList />} />
      <Route path="/post/:id" element={<DetailsPage />} />
      <Route path="/inquiry" element={<InquiryPage />} />
    </Routes>
  );
};

React.FC: TypeScriptの型エイリアスで、React Functional Componentを示します。
constによるコンポーネントを定義できる型です。
(各ファイルにも同様に、関数コンポーネントに型付けする)

posts.ts (記事のデータ)

type Posts = {
  id: number;
  title: string;
  thumbnailUrl: string;
  createdAt: string;
  categories: string[];
  content: string;
}

export const posts: Posts[] = [....]

TypeScriptの型エイリアスで、複合的な型を纏めたもの。
型ごとに毎度表すのは大変なので、型エイリアスを活用すると便利です!
・このファイルのみ、.ts拡張子になっていますが、JSXを含まないファイルなので.tsにしています。
  JSXを含む場合は、.tsx拡張子に指定すべきです。

PostsList.tsx (記事一覧ページ)

type ArticleType = {
  id: number;
  createdAt: string;
  categories: string[];
  title: string;
  content: string;
}

type PostsType = {
  posts: ArticleType[];
}

ArticleTypeという名前の型を定義し、
その中に各プロパティ(posts.tsの記事データ)の型を指定しています。

posts: ArticleType[]:
postsはArticleTypeの配列であることを示しています。つまり、この型は複数の記事を含むオブジェクトを表します。

const formatDate = (dateString: string) => {
    const date = new Date(dateString);
    const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'numeric', day: 'numeric' };

    return date.toLocaleDateString('ja-JP', options);
  };

const formatDate = (dateString: string)
引数dateStringstring型を付与。
TypeScriptでは、「変数: 型」の形式を型アノテーションを付与するのが基本ですが、
全ての変数に型を付与しなければいけないわけではありません。

例)let age = 30;
変数ageでは、ageは数値であることは明らかなので、「age: number」としたのと等価と
見なされます。

const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'numeric', day: 'numeric' };

Intl.DateTimeFormatOptions:
これはIntl.DateTimeFormatオブジェクトのオプションの型

const [posts, setPosts] = useState<PostsType>({ posts: [] })
const [loading, setLoading] = useState<boolean>(true);

<PostsType>: postsの型をPostsTypeと指定しています。これにより、postsPostsType型のデータを持つことをTypeScriptに示します。

<boolean>: loadingの型をbooleanと指定しています。
これにより、loadingが真偽値を持つ意味になります。

useEffect(() => {
  const fetchData = async () => {
    try {
      const response = await fetch("https://1hmfpsvto6.execute-api.ap-northeast-1.amazonaws.com/dev/posts");
      const data = await response.json() as PostsType;
      setPosts(data);
    } finally {
      setLoading(false); // データ取得が完了したらローディングを終了
    }
  };

  fetchData();
}, []);

as PostsTypeは、変換したデータがPostsType型であることをTypeScriptに示します。

DetailsPage.tsx (記事詳細ページ)

type detailsType = {
  id: number;
  createdAt: string;
    thumbnailUrl: any;
  categories: string[];
  title: string;
  content: string;
};

type ApiResponse = {
  post: detailsType;
}

type detailsType
detailsTypeという名前で型を定義。
その中に各プロパティの型をそれぞれ指定。

type detailsType = {
  id: number;
  createdAt: string;
    thumbnailUrl: any;
  categories: string[];
  title: string;
  content: string;
};

type ApiResponse = {
  post: detailsType;
}

const DetailsPage: React.FC
 関数コンポーネントに、React.FC型を付与。

post: detailsType
 このオブジェクトはdetailsType型のデータを持つことを意味します。

InquiryPage (お問い合わせフォーム)

type InquiryType = {
  name: string ;
  email: string;
  message: string;
};

type ErrorsType = {
  name?: string;
  email?: string;
  message?: string;
};

type ErrorsType
type ErrorsTypeという名前の型を定義。

name?: string, email?: string, message?: string;
  この型定義は、フォームのエラーメッセージを管理するためのオブジェクトの構造を定義。
?マークはオプショナルプロパティを示すTypeScriptのシンタックスです。
ユーザーが名前, email, messageを入力しなかった場合に表示するエラーメッセージを格納します。

const InquiryPage: React.FC = () => {
  const [inquiryData, setInquiryData] = useState<InquiryType>({
    name: "",
    email: "",
    message: "",
  });
  const [errors, setErrors] = useState<ErrorsType>({});
  const [isSubmitting, setIsSubmitting] = useState<boolean>(false);

InquiryType: inquiryData
InquiryType: inquiryDataの型をInquiryTypeと指定しています。これにより、inquiryDataInquiryType型のデータを持つことをTypeScriptに示します。

・ErrorsType: errorsの型をErrorsTypeと指定しています。これにより、errorsErrorsType型のデータを持つことをTypeScriptに示します。

const handleChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    const { id, value } = e.target;
    setInquiryData((prevData) => ({ ...prevData, [id]: value }));
  };

e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
 イベントハンドラーの部分なので、eの型はReact.ChangeEventで定義。
 HTMLInputElementまたはHTMLTextAreaElementから発生するイベントである
ことをTSに示しています。

const validate = () => {
    const tempErrors: ErrorsType = {};
    if (!inquiryData.name) tempErrors.name = "お名前は必須です。";
    if (!inquiryData.email) tempErrors.email = "メールアドレスは必須です。";
    if (!inquiryData.message) tempErrors.message = "本文は必須です。";
    setErrors(tempErrors);
    return Object.keys(tempErrors).length === 0;
  };

ErrorsType: tempErrors
ErrorsType: tempErrorsの型をErrorsTypeと指定しています。これにより、tempErrorsErrorsType型のデータを持つことをTSに示します。

const handleSubmit = async (e: FormEvent): Promise<void> => {
    e.preventDefault();
    if (!validate()) return;

    setIsSubmitting(true);

(e: FormEvent)
eの型をFormEventで指定。FormEventはフォームの送信イベントを表します。

: Promise<void>
戻り値の型。
つまり、この関数はPromiseを返し、そのPromiseは何も値を返さないことを示しています。

ポイント

・静的型付け
 変数や関数の引数、戻り値に対して型を明示することで、コードの品質と保守性が向上します。型情報を利用することで、コンパイル時に多くのエラーを検出でき、予期しないバグを防ぐことができます。

・型注釈
 変数や関数のパラメータに対して、型を明示的に指定することを意味します。
例)

let integer: number = 10;

・型推論
 明示的な型注釈を省略できるため、コードをより簡潔で読みやすくすることができます。
特に、文脈から変数の型が明らかな場合に有効になります。
例)

let integer2 = 10;

おわり

JavaScriptをTypeScriptに置き換えて実装してみました!
TSは需要ある言語ですし、大規模な開発で使われるので取得して損はないですね!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0