はじめに
こんにちは!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)
引数dateString
にstring
型を付与。
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
と指定しています。これにより、posts
がPostsType
型のデータを持つことを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
と指定しています。これにより、inquiryData
がInquiryType
型のデータを持つことをTypeScriptに示します。
・ErrorsType: errorsの型をErrorsType
と指定しています。これにより、errors
がErrorsType
型のデータを持つことを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
と指定しています。これにより、tempErrors
がErrorsType
型のデータを持つことを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は需要ある言語ですし、大規模な開発で使われるので取得して損はないですね!