【7章】TypeScript基礎学習+演習の振り返り
公開: 2026年01月18日
今回、初めてTypeScriptに触れました。
いざ書き始めると「なんとなく」で動かしていた部分がエラーとして炙り出されて、自分の理解不足を突きつけられました。 でも、その分「なるほど!」という発見も多かったです。
基本的な型定義以外で、6章から設計を切り替えたポイントをまとめておこうと思います。
最初はReact Hook Formの標準機能だけでバリデーションを書いていたんですが、途中で Zod に切り替えました。 一番のメリットは、データの形とチェックルールを一箇所にまとめられること。これまではバラバラに書いていたものが、Zodだと「このデータはこういう形!」と一発で定義できるのが気持ちよかったです。また、後述する型アサーション(as)を極力使いたくなかったのも切り替えた理由の一つです。
ここで一つハマったのが、バリデーションの順番です。 メールアドレスを空にした時、本当は「必須です」と出してほしいのに、「形式が違います」が優先されてしまう。
Zodは「上から順番に実行される」という仕様を知って、.pipe() を使って順序を整理したことで、ようやく理想の動きになりました。
Zod v4からはトップレベルの関数(z.email()など)が推奨されるなど仕様変更もあり、単純に .string().email() と繋げると非推奨エラーが出てしまい苦戦しましたが、パイプラインで繋ぐ方法で解決できました。
//実装コード
email: z
.string()
.min(1, "メールアドレスは必須です。")
.pipe(z.email("メールアドレスの形式が正しくありません。")),
単にエラーを防ぐだけじゃなくて、「どういう順番でメッセージを出せばユーザーが使いやすいか」を考えるのも設計なんだな、と勉強になりました。
UIパーツ(InputやButton)を作る時に学んだのがComponentPropsです。
前章のコードレビューで教わりましたが、TypeScriptへの変換を実践することでイメージがより具体化されました。 わざわざ自分で型を書き並べなくても、HTMLタグが本来持っている機能をそのまま活かせる感覚がわかってからは、非常に便利だなと実感しています。
//実装コード(src/components/ui/Button.tsx)
import type { ComponentProps } from "react";
interface ButtonProps extends ComponentProps<"button"> {
variant: "primary" | "secondary";
}
export default function Button({
children,
variant = "primary",
className = "",
...props
}: ButtonProps) {
const baseStyle =
"min-w-30 px-4 py-3 font-bold rounded-lg transition shadow-md cursor-pointer disabled:cursor-not-allowed";
const variants = {
primary: props.disabled
? "bg-gray-400 text-white"
: "bg-[#1e293b] text-white hover:opacity-90",
secondary:
"bg-[#e2e8f0] text-gray-800 hover:bg-gray-300 disabled:opacity-50",
};
return (
<button
className={`${baseStyle} ${variants[variant]} ${className}`}
{...props}
>
{children}
</button>
);
}as に頼ると後が怖い学習の過程で、TypeScriptに「これはこの型だよ」と無理やり教え込む as(型アサーション)の危険性も学びました。 as を使えば型エラーは消えます。
しかし、それは「開発者がTypeScriptの口を塞いでいる」状態であり、本来防げるはずのミスを見逃す不安を感じました。
デメリット
実際のデータ構造が型とズレていてもエラーが出ず、実行時にアプリが突然クラッシュする原因になるみたいです
今回は as で嘘をつくのではなく、Zodの .safeParse() を使い、データが不正な場合は適切にエラーハンドリングする設計を徹底しました。実際、実務でどこまで as の使用を許容するかのラインはまだ手探りなので、今後より多くの現場コードに触れて学んでいこうと思います。
機能が増えるにつれて、インポートのパスが ../../../ みたいに長くなって「どこに何があるの状態」に。そこで調べてみると「バレルエクスポート(index.ts)」なるものを知りました。
各フォルダに index.ts を置いて窓口を一つにするだけで、インポートがスッキリ! types や utils を細かく分けつつ、この「窓口一本化」をやるだけで、見やすくなりました。
今回の最終的なファイル構成は以下の通りです。
src/
├── components/ # UIパーツや機能ごとのコンポーネント
│ ├── ui/ # ボタンや入力欄などの汎用パーツ
│ ├── posts/ # 記事一覧・詳細に関連する部品
│ └── contact/ # フォームに関連する部品
├── hooks/ # API通信などのロジック(Custom Hooks)
├── types/ # Zodスキーマと型定義
│ └── index.ts # バレルエクスポートで窓口を一本化
├── utils/ # サニタイズなどの共通関数
│ └── index.ts # バレルエクスポートで窓口を一本化
├── constants/ # APIのURLなどの定数管理
├── pages/ # 各画面のルートコンポーネント
└── vite-env.d.ts # 環境変数の型を保護型は「防波堤」
APIから来るよくわからないデータをチェックして、安全な状態にしてから使う。
仕様まで遡って考える
実装がうまくいかない時、単に動くコードを探すのではなく、「そもそもZodはどういう順序で処理しているのか」という仕様の部分を調べたことで、応用が効く知識になりました。
メンテナンス性を意識するindex.ts の作成や定数の分離は、書いている瞬間は手間ですが、後で修正する時に「1箇所直せば済む」という安心感に繋がります。
「未来の自分が楽をするための投資」だと考えて取り組めました。
演習を通して一番感じたのは、「今の自分がサボらず丁寧に書けば、未来の自分が楽をできる」ってこと。 最初は面倒に感じた型定義も、慣れてくると「TypeScriptが守ってくれてる」という安心感に変わりました。
