30代SEの個人開発ログ

30代SEが、個人開発・ゲーム制作・技術学習の試行錯誤を記録するブログです。

ChatGPTを使いながら、ポケモン金銀の相性チェッカーをNext.jsで作って公開した

最近、レトロゲームを遊ぶときに使える自分用ツールを作りたくなり、ポケモン金銀向けの相性チェッカーを作りました。

公開したものがこちらです。
https://pokemon-type-checker-for-goldsilver.hh08.workers.dev/

作ったもの

ポケモン金銀に登場するポケモンのタイプ相性を確認するためのツールです。
ポケモンを選ぶと、そのポケモンに対して各攻撃タイプが何倍になるかを一覧で確認できるようにしています。

なぜこれを作ったのか

わざわざ作らなくても、この手のツールはごまんとあります。
それでも作ろうと思ったのは、「自分にとって役立つものを作る」と決めていたからです。

これまで技術書やUdemyなどでいろいろ勉強してきましたが、教材どおりに作ることはできても、そこから先に自分なりに広げることがなかなかできませんでした。そして、勉強したこと自体に満足して終わってしまうことも多かったです。

そこで今回は、「勉強のために何かを作る」のではなく、「自分にとって本当に役立つものを作る」ことを出発点にしました。そうすることで、学んだ内容を実際の形に広げてみたかったのです。

実際にやってみると、この進め方はかなり良かったと思います。
作る目的がはっきりしているので、途中で詰まっても調べる意味があるし、何より完成したときの満足感も大きかったです。

ChatGPTの使い方

今回の開発ではChatGPTをかなり使いました。
ただしコード生成を丸投げではなく、機能を小さく分けて少しずつ作るようにしました。

あくまで設計の主導権は自分が持つことを意識し、分からない部分はその都度確認しながら進めました。その結果、「仕組みを理解しながら動くものを作れた」という感覚があります。

今後は、コード生成そのものをAIが担う場面がますます増えていくと思います。
その中で重要になるのは、中身を理解したうえで適切に指示を出し、必要に応じて修正や判断ができる人間だと感じています。SEとして生き残るうえでも、その力は大きな指標になると考えています。

技術構成

今回使った技術はかなりシンプルです。

  • Next.js
  • React
  • TypeScript
  • Cloudflare Workers

データは、ポケモン一覧とタイプ相性表を JSON / TypeScript の形で持たせて、画面側で計算して表示しています。サーバー側で複雑な処理をする必要がないので、小さなツールとしてはこの構成がちょうどよかった。

作ってみて感じたこと

自分は39歳で、仕事ではExcelやドキュメントを扱う場面も多く、個人でWebツールを1本作って公開するような機会はほとんどありません。それでも今回、自分で仕様を決めて、小さく作って、最後に公開までできたのはかなり良い経験になりました。

教材をなぞる学習も大事ですが、自分で使いたいものを決めて作ると、学習と実装がちゃんとつながる。今回はその感覚を久しぶりに強く持てました。

今後も、こうした「自分にとって役立つ小さなツール」を少しずつ形にしていきたいと思います。

TypeScriptのtypeとinterfaceの違いを整理してみた【ユニオン型も解説】

こんにちは。今日は TypeScript の typeinterface の違い、そしてよく一緒に出てくる ユニオン型 についてまとめます。
自分の勉強メモも兼ねて書いているので、これから TypeScript を学ぶ人の参考になれば嬉しいです。

typeinterfaceの共通点
  • どちらも 型を定義するために使える
  • オブジェクトの形を表したり、再利用性を高めることができる

一見すると「どっちを使っても同じじゃない?」と思うかもしれませんが、実は特徴が少し違います。

typeの特徴
  • ユニオン型やプリミティブ型のエイリアスに使える
  • &(インターセクション)を使えば型を合成できる
  • ただし 宣言マージはできない

例:ユニオン型の定義

export type AccountingType = 
  | "収入" 
  | "支出(現金)" 
  | "支出(クレジットカード)";

この場合、AccountingType は 3つの文字列のどれか を意味します。

interfaceの特徴
  • 主にオブジェクトの形を表現するのに使う
  • extends で他のインターフェースを継承できる
  • 宣言マージが可能 → 後から同じ名前で再定義すると自動で結合される

例:オブジェクト型の定義

export interface Accounting {
  id: string;
  date: string;
  amount: number;
  content: string;
  type: AccountingType;
  category: IncomeCategory | ExpenseCategory;
}
宣言マージの違い

interface の場合

同じ名前で再宣言すると自動で結合されます。

interface Accounting {
  id: string;
  amount: number;
}

// 後から拡張できる!
interface Accounting {
  memo: string;
}

const a: Accounting = {
  id: "1",
  amount: 1000,
  memo: "昼ごはん代"
};

あとから自由にフィールドを追加できるのが大きな特徴。

type の場合

同じ名前で再宣言はできません。

type Accounting = {
  id: string;
  amount: number;
};

// 再宣言するとエラー
// type Accounting = { memo: string };

// 拡張するなら & を使う
type AccountingWithMemo = Accounting & { memo: string };

const b: AccountingWithMemo = {
  id: "2",
  amount: 2000,
  memo: "家賃"
};

type は その場で閉じた型を作るイメージです。

ユニオン型とは?

ユニオン型は「どれかひとつ」を表す型です。
| を使います。

let value: string | number;

value = "こんにちは"; // OK
value = 123;         // OK
value = true;        // エラー

今回の例だと、IncomeCategory | ExpenseCategory がまさにユニオン型です。
「収入カテゴリ」か「支出カテゴリ」のどちらか、という意味になります。

まとめ

type

  • ユニオン型やプリミティブ型に強い
  • 拡張するときは & を使う

interface

  • オブジェクトの型を表すのに便利
  • 宣言マージがあるので後から拡張できる

ユニオン型

  • 「型の選択肢を列挙」できる便利な仕組み

僕の場合、

  • 固定的に使う型 → type
  • 将来拡張したい型 → interface

という感じで使い分けています。
TypeScript の型は慣れると本当に強力なので、ぜひいろいろ試してみてください。

React × TypeScript × FullCalendar 開発での注意点

先日、React × TypeScript × FullCalendar を使って開発していたときに、以下のようなエラーに遭遇しました。

'DatesSetArg' は型であり、'verbatimModuleSyntax' が有効であるときは、
型のみのインポートを使用してインポートされる必要があります。ts(1484)

最初は「何これ?」と戸惑ったのですが、調べてみると TypeScript 5.0 以降の仕様変更 が関係していました。今回はこのエラーの概要、原因、そして対策方法をまとめます。

エラーが出たコード例

FullCalendar の DatesSetArg 型をインポートして使おうとしたときにエラーが発生しました。

import dayGridPlugin from "@fullcalendar/daygrid";
import FullCalendar from "@fullcalendar/react";
import { DatesSetArg } from '@fullcalendar/core' // ←ここでエラー

const Calendar = () => {
  const handleDateSet = (datesetInfo: DatesSetArg) => {
    const currentMonth = datesetInfo.view.currentStart
  }
}
エラーの原因

TypeScript 5.0 から導入された verbatimModuleSyntax がポイントです。
これを true にしていると、

を明確に分けてインポートする必要があります。
今回 DatesSetArg型だけ なので、普通の import を使うとエラーになってしまいました。

解決方法

解決方法は大きく2つあります。

1. import type を使う

型だけをインポートする場合は import type を使えばOKです。

import type { DatesSetArg } from '@fullcalendar/core'

これでエラーは解消されますし、「これは型だよ」というのが明確になるので、将来的にもおすすめの方法です。



2. tsconfig.jsonverbatimModuleSyntax を無効にする

もし「いちいち import type を書くのが面倒」という場合は、tsconfig.json で設定をオフにする方法もあります。

{
  "compilerOptions": {
    "verbatimModuleSyntax": false
  }
}

これで従来通りの挙動に戻せます。

まとめ
  • TypeScript 5.0 から 型と値のインポートを区別 する必要がある
  • DatesSetArg は「型」なので、普通の import だとエラーになる
  • 解決方法は
  1. import type を使う
  2. tsconfig.jsonverbatimModuleSyntax: false にする

自分は今回、素直に import type に修正して解決しました。
同じように TypeScript 5.0 以降で「急にエラーが出た!」と困っている方の参考になれば幸いです!

createThemeでデザインルールを一元管理する

フロントエンド開発では、見た目を整えるためにCSSやUIフレームワークを組み合わせて使うことが多いですが、その中でも人気が高いのが MUI (Material UI) です。
MUIは、Googleが提唱する Material Design に基づいたコンポーネントを提供してくれるライブラリで、Reactとの相性も非常に良いのが特徴です。

この記事では、MUIのインストールからテーマ機能である createTheme の役割までを解説します。

MUIのインストール

まずはプロジェクトにMUIを追加します。
npm install @mui/material @emotion/react @emotion/styled

  • @mui/material: MUIの本体パッケージ
  • @emotion/react / @emotion/styled: MUIが内部で利用しているCSS-in-JSライブラリ

これらをインストールすることで、MUIのコンポーネントをReactで利用できるようになります。

MUIのコンポーネントを使ってみる

インストール後、まずはボタンを表示してみましょう。

import Button from '@mui/material/Button';

function App() {
  return <Button variant="contained">ADD EVENT</Button>;
}


このコードを実行すると、MUI標準のスタイルが適用されたボタンが表示されます。
CSSを一切書かずに、洗練されたUIを利用できるのがMUIの大きな利点です。

createThemeとは?

MUIを使っていると必ず出てくるのが 「テーマ (theme)」 という概念です。

テーマの役割

といった、アプリ全体のデザインルールをひとまとめに管理できます。
このテーマを生成するのが createTheme です。

createThemeの基本例
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { CssBaseline, Button } from '@mui/material';

const theme = createTheme({
  palette: {
    primary: {
      main: '#1976d2', // プライマリカラー
    },
    secondary: {
      main: '#9c27b0', // セカンダリカラー
    },
  },
  typography: {
    fontFamily: 'Roboto, Arial, sans-serif',
    fontSize: 14,
  },
});

function App() {
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <Button variant="contained" color="primary">プライマリ</Button>
      <Button variant="outlined" color="secondary">セカンダリ</Button>
    </ThemeProvider>
  );
}


ポイント

  • createTheme(...) でテーマオブジェクトを作成
  • ThemeProvider でアプリ全体をラップすると、そのテーマが全てのMUIコンポーネントに適用
  • CssBaseline を追加すると、ブラウザごとのデフォルトCSSをリセットし、統一的な見た目を実現
さらにカスタマイズ

MUIのテーマは、単に色やフォントを設定するだけでなく、独自のプロパティを追加して拡張 することもできます。
例えば「未着手」「進行中」「完了」といった、アプリ固有の色をテーマに組み込んでおけば、アプリ全体で一貫したカラールールを適用できます。

MUIでは @mui/material/styles を拡張して型定義を追加することで、createThemepalette に独自のキーを生やせます。

// MUIのテーマ拡張を行うための型定義
declare module "@mui/material/styles" {
  // theme.palette に独自の色を追加
  interface Palette {
    todoColor: PaletteColor;
    inProgressColor: PaletteColor;
    doneColor: PaletteColor;
  }

  // createTheme の引数に渡す型も拡張
  interface PaletteOptions {
    todoColor?: PaletteColorOptions;
    inProgressColor?: PaletteColorOptions;
    doneColor?: PaletteColorOptions;
  }
}

export const theme = createTheme({
  typography: {
    fontFamily: 'Noto Sans JP',
    fontWeightRegular: 400,
    fontWeightMedium: 500,
    fontWeightBold: 700,
  },

  palette: {
    // 未着手の色
    todoColor: {
      main: blue[500],
      light: blue[100],
      dark: blue[700],
    },
    // 進行中の色
    inProgressColor: {
      main: red[500],
      light: red[100],
      dark: red[700],
    },
    // 完了の色
    doneColor: {
      main: green[500],
      light: green[100],
      dark: green[700],
    },
  },
});

ポイント

  • declare module "@mui/material/styles" を使って Palette / PaletteOptions を拡張すると、TypeScriptで型補完が効くようになる
  • theme.palette.todoColor.main のようにアクセス可能になり、アプリ全体で一貫したカラールールを適用できる
  • ドメインに合わせて「アプリ固有の色」をテーマに登録することで、スタイルの管理がシンプルになる

レスポンシブ対応
テーマに含まれる breakpoints を使うと、画面サイズごとにスタイルを切り替え可能です。

sx={{
  fontSize: {
    xs: '0.8rem', // モバイル
    sm: '1rem',   // タブレット以上
    md: '1.2rem', // デスクトップ以上
  }
}}
まとめ
  • MUIを導入すると、Material Designに沿ったUIをすぐに使える
  • createTheme は「アプリ全体のデザインルール」をまとめる仕組み
  • ThemeProvider でラップすれば、全コンポーネントに統一的なスタイルが反映される
  • 色・文字・余白・レスポンシブ・コンポーネントごとのデフォルトを一括で管理可能

デザインを手作業で整えるのは大変ですが、MUIと createTheme を使えば 「開発スピード」と「統一感のあるUI」 を両立できます。

TypeScriptエラー対策:MUI v7 Grid APIの変更に注意

MUIを使って以下のようなコードを書いたところ、TypeScriptでエラーが出ました。

import { Card, CardContent, Grid, Typography } from "@mui/material";

const MonthlySummary = () => {
  return (
    <Grid container spacing={{ xs: 1, sm: 2 }} mb={2}>
      <Grid xs={4} display="flex" flexDirection="column">
        <Card sx={{ color: "white", borderRadius: "10px", flexGrow: 1 }}>
          <CardContent>
            <Typography variant="h6">Summary Title</Typography>
            <Typography variant="body2">Summary content goes here.</Typography>
          </CardContent>
        </Card>
      </Grid>
    </Grid>
  );
};

export default MonthlySummary;

出てきたエラーメッセージはこちらです。

この呼び出しに一致するオーバーロードはありません。
  ...
    プロパティ 'component' は型 '{ children: Element; xs: number; display: "flex"; flexDirection: "column"; }' にありませんが、
    型 '{ component: ElementType<any, keyof IntrinsicElements>; }' では必須です。
  ...
    プロパティ 'xs' は型に存在しません。

つまり、xs が存在しないとか component が必須と怒られてしまいました。

原因

調べてみると、これは MUI v7 で Grid のAPIが変更されたことが原因でした。

旧Grid(v5まで / v6 Legacy)

  • 子要素に item を書く必要がある
  • サイズ指定は xs={4} などで行う

新Grid(v7以降)

  • 子要素は常に item 扱い → item は不要
  • サイズ指定は size={{ xs: 4 }} または size={4} に変更

今回、自分は import { Grid } from "@mui/material"; で 新しいGrid を読み込んでいるのに、書き方を旧Gridのままにしていたため、型エラーが出ていました。

解決方法

エラーを直す方法は2通りあります。

1. 新しいGridに合わせる

<Grid container spacing={{ xs: 1, sm: 2 }} sx={{ mb: 2 }}>
  <Grid size={{ xs: 4 }} display="flex" flexDirection="column">
    ...
  </Grid>
</Grid>

ポイント

  • 子は item を書かない
  • xs ではなく size を使う

2. 旧Grid(GridLegacy)を使う

もしこれまで通り xs={4}item を使いたいなら、GridLegacy をインポートします。

import Grid from "@mui/material/GridLegacy";

<Grid container spacing={2} sx={{ mb: 2 }}>
  <Grid item xs={4}>
    ...
  </Grid>
</Grid>
まとめ
  • MUI v7 から Grid のAPIが刷新され、xs → size に変更された
  • APIを使いたければ GridLegacy をインポートする
  • 新しいAPIに寄せるのがおすすめ



自分は最初「component が必須?」「xs が存在しない?」と混乱しましたが、調べてみたら単純にGridの世代が違うだけでした。
同じようにハマった人の参考になれば幸いです!

Reactコンポーネントの書き方とexport方法の違い

Reactコンポーネントの書き方にはいくつかパターンがあります。
特に 関数の書き方 と export方法(default / named) の違いは、最初は分かりにくいところです。

この記事では、

  • default export(後出し)の意味と使いどころ
  • 巻き上げ(hoisting)とは何か
  • 関数の書き方+exportの違いと使い分け

をわかりやすくまとめます。


1. export の種類

default export(デフォルトエクスポート)

  • 1ファイルにつき1つだけ
  • import時に好きな名前を付けられる
export default function App() {
  return <div>App</div>
}

// import例
import MyApp from './App'

named export(名前付きエクスポート)

  • 1ファイルにいくつでも書ける
  • import時は名前を合わせる必要がある
export const Home = () => <h1>Home</h1>
export const About = () => <h1>About</h1>

// import例
import { Home, About } from './pages/Home'


2. 関数の書き方の2種類

関数宣言(function declaration)

function App() {
  return <div>App</div>
}

巻き上げ(hoisting) がされる
→ ファイル内で宣言より前に呼び出しても使える

関数式(アロー関数)

const Home = () => {
  return <div>Home</div>
}

巻き上げされない
→ 宣言より前で呼び出すとエラーになる


3. 巻き上げ(hoisting)とは?

JavaScriptでは、関数宣言や変数宣言が実行前にファイルの先頭に持ち上がったように扱われる仕組みがあります。

例(関数宣言はOK):

sayHello() // ✅ 動く

function sayHello() {
  console.log("Hello")
}

例(アロー関数はNG):

sayHello() // ❌ エラー

const sayHello = () => {
  console.log("Hello")
}


4. default export 後出し とは?

関数を先に宣言し、最後に export default する書き方です。

// 先に変数や関数などの準備
const appTitle = "My Cool App"

function App() {
  return <h1>{appTitle}</h1>
}

// 最後にdefault export
export default App

この書き方のメリット:

  • コンポーネントの前に変数や補助関数を置ける
  • 同じファイル内で複数の関数を定義し、最後に主役だけexportできる


5. まとめ

  • default export … 主役コンポーネント向き。import時に名前を自由に決められる
  • named export … 複数コンポーネントや関数をまとめて管理するのに便利
  • 関数宣言 は巻き上げされるが、アロー関数はされない
  • default export後出し は、処理や補助関数を前に書いて最後に主役だけexportしたいときに使う

ReactでSPA構築: react-router-domの基本的な使い方

ReactでSPA(シングルページアプリケーション)を作るとき、ほぼ必須になるのがreact-router-domです。
今回は基本的な使い方から、AppLayoutを使った共通レイアウトの作成方法までまとめていきます。


1. react-router-domとは?

react-router-domReact用のルーティングライブラリ です。

「ルーティング」とは、URLに応じてどのコンポーネントを表示するかを切り替える仕組みのこと。

Reactは単一ページ(SPA)なので、ページ遷移に見えても実際はコンポーネントの切り替えで画面を更新しています。その切り替えを管理してくれるのが react-router-dom です。


2.インストール方法

npm i react-router-dom

www.npmjs.com


3.よく使うコンポーネント・フック

コンポーネント/フック 役割
BrowserRouter 履歴管理付きのルーター。普通のWebアプリはこれを使う
Routes ルート定義をまとめる
Route URLパスとコンポーネントを紐付け
Link アプリ内リンク(<a>タグ代わり)
Outlet ネストされたルートの表示位置
Navigate コードからページ遷移(リダイレクト)
useParams URLパラメータ取得
useNavigate ページ遷移用の関数取得

 


4.ルーティング例

<Route index>は「親ルートにアクセスしたときのデフォルトの子ルート」を設定するための属性です。

上記の例では

  • /にアクセスすると自動的に<Home />が表示される
  • /reportにアクセスすると<Report />が表示される

という動きになります。


5.共通レイアウトを作るAppLayout

ルーティング例を見ていただくと、AppLayoutコンポーネントが親ルートとしてあり、Homeコンポーネント、ReportコンポーネントがAppLayoutコンポーネントの子ルートとして配置されています。

なぜAppLayoutを用意するのか?

  • ヘッダーやフッターなど全ページ共通UIをまとめて管理できる
  • <Outlet />で子ルートの表示位置を制御できる
  • 認証チェックやエラーハンドリングを一括で適用できる

コードの動き

  • <Outlet />の場所に子ルートのコンポーネントが差し込まれる
  • /<Home />/report<Report />が表示される
  • AppLayoutコンポーネントにヘッダーやナビゲーションを書けば常に共通で表示される

6.シングルページアプリケーション(SPA)とは

Single Page Applicationの略で、最初に読み込むHTMLは1枚だけで、ページ遷移はJavaScriptで中身だけ差し替える仕組みのWebアプリのことです。

MPA(Multi Page Application)との違い

特徴 SPA MPA
HTML読み込み 最初の1回のみ ページごとに読み込み
ページ切り替え JSで中身だけ切り替え ページ全体を再読み込み
表示速度 高速 遅め
SEO 対策が必要 標準で有利

Reactでページ切り替えを行うには、react-router-domなどのルーターライブラリを用いることで実現します。


7.まとめ

  • react-router-domはReactでのページ切り替えを管理するライブラリ
  • BrowserRouterRoutesRouteが基本
  • 共通UIはAppLayoutにまとめ、<Outlet />で子ルートを挿入
  • Route indexは親ルートのデフォルト表示を設定できる

Reactでアプリを作るとき、ルーターを正しく理解しておくと画面設計がグッと楽になりそうです。