2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

脱・写経 動画教材の"1歩先"へ

Posted at

目的

  • 動画教材 ⇒ 写経になりがち
    • 「分かったつもり」で終わってしまうの意見を見かけて納得する
  • 「なら写経じゃない部分を自分で追加すればいいのでは?」← イマココ!

目標

  • JavaScript を TypeScript で再構築しながら、一歩深く React の理解を深める!
    • js⇒ts へのリライト業務にも対応可能な経験も身に着く!

記事を書いてる人

  • フロントエンド分野で転職するためにスキルアップを目指してる人
  • TypeScript の基礎と Sass の基礎をやったので、実践環境として React に本格挑戦し始めた
  • 本記事は学習のアウトプットも兼ねて執筆
  • 転職活動向けポートフォリオの一環

目次

今回の使用教材

【React アプリ開発】3 種類の React アプリケーションを構築して、React の理解をさらに深めるステップアップ講座

└ 【アプリ ①】ポケモン図鑑アプリケーションを構築してみよう

環境

基本環境

項目 概要 バージョン
OS 基本環境 Windows11
WSL 仮想環境 Ubuntu 24.04.1
Node.js ランタイム 22.14.0
npm パッケージ管理 10.9.2

フロントエンド & 言語拡張

項目 概要 バージョン
TypeScript 型付け言語 5.8.3
typescript-eslint リンター 8.44.0
React UI ライブラリ 19.1.1
react-dom DOM 操作 19.1.1
vite ビルドツール 7.1.7

CSS

項目 概要 バージョン
Sass メタ言語 1.93.2
sass-loader 連携ツール 16.0.5
browser-sync ライブリロード 3.0.4
cssnano 圧縮・最適化 7.1.1
postcss-sort-media-queries PostCSS プラグイン 5.2.0
  • 使用 AI: Gemini
    • 手頃なところを使用している間に、構築済み環境などの情報が 1 スレッドに非常に蓄積されていたので継続利用

「脱・写経」の取り組み(差分と思考のまとめ)

1. JavaScript を TypeScript でコーディング

教材の元コード

  • 特筆して無し

やったこと

  • 教材は js × React で進行
    • ts の練習教材にするのがそもそもの目的のため ts のコーディング法に全部自力で修正
  • 静的型付け処理・定義は全部自力でコーディング
    • 序盤の特にオブジェクトの型付けは AI にも頼った
  • オブジェクトの型設定用にtypes.tsxを切り出して用意
    • オブジェクト構造に型を付けるのでinterfaceで定義(type エイリアスより適切と判断)
    • 外部で呼び出す型はexportを付与して外部出力設定
    • 内部処理に使用するだけの型は export を付与しない
    • 複雑な型は一部を別interfaceに切り出す
      • 複数回オブジェクトに登場する場合は型[]で配列扱いにする

型付け処理の例

  • ここの型定義は内容のデータが長かったため、ほぼ AI の解析に投げてます
  • 取り溢している型も多かったので、実データと突き合わせて手動で検証・リファインしました
// types.tsx:ポケモンAPIのレスポンス構造をTypeScriptで定義

/**
/* 各ポケモンの個別データ
*/

// 技の情報を表す型 (例: "scratch", "cut")
interface PokemonMove {
  move: {
    name: string; // 技の名前
    url: string; // 技の詳細URL
  };
}

// ステータスを表す型 (例: HP, Attack, Defense)
interface PokemonStatus {
  base_stat: number; // 基本的なステータス値
  stat: {
    name: string; // ステータスの名前 (hp, attack, defense など)
  };
}

// ポケモン個別の詳細データを表すメインの型
export interface PokemonDetail {
  id: number; // ポケモンID (1, 2, 3...)
  name: string; // ポケモンの名前 (bulbasaur, ivysaur...)
  height: number; // ポケモンの高さ (デシメートル単位)
  weight: number; // ポケモンの重さ (ヘクトグラム単位)
  sprites: {
    front_default: string; // デフォルトの画像URL
  };
  moves: PokemonMove[]; // 覚える技のリスト(PokemonMove型の配列)
  stats: PokemonStatus[]; // ステータスのリスト(PokemonStatus型の配列)
}

オブジェクト構造の図解

  • ※AI 生成

所感・小括

  • 既成の API の構造解析が難しい!

    • 簡単な型で練習の機会を作る
    • 今後は API レスポンスの構造を視覚化してから型設計に入るよう意識したい
  • 関数や Promise(後述)での型付けが難しい

    • AI に処理の仕方を質問する回数を減らすのが当面の目標
      • 過去に記述した定義・型付けのコードを見る
      • 自力検索 など
  • 学び:コードの安全性の担保という型定義は大変

2. コードの補完

教材の元コード

  • 教材は js 記法
  • fetchで外部 API 通信を行っている
    • 通信失敗の可能性もある
export const getAllPokemon = (url) => {
  return new Promise((resolve, reject) => {
    fetch(url)
      .then((res) => res.json())
      .then((data) => resolve(data));
  });
};
  • 教材「rejectは fetch が失敗した時の処理ですが、今回の教材では設定する必要はないです(要約)」
  • 私「いや半端で気持ち悪いからちゃんと書くわ」

やったこと

  • catchで失敗処理を追加
  • catchで引数に指定されてるerrorって型、何?
    • Error型があることを知る
    • 一方でエラー内容によっては別の型になるので any や自動推論に任せてしまってもいいらしい

変更後のコード

  • ts 記法
  • 失敗パターンの処理を追加
/* パーツとして使用する関数を記述 */
import type { PokemonListResponse } from './types'; // ユーザー定義型を読み込む(type{型})

/*** @name getAllPokemon
 *   @function
 *   @type PokemonListResponse
 *   @param url
 *   @return
 *  ポケモンAPIからデータを取得する
 *  getPokemon()(後述)のようにasync/await記法もできるが、練習のため.then()構文を使用
 */

export const getAllPokemon = (url: string): Promise<PokemonListResponse> => {
  // resolve:成功
  // resolve:失敗
  // Promise:fetch以下の処理が終わるまで待機
  return new Promise<PokemonListResponse>((resolve, reject) => {
    // fetchで引数のURLに対しAPIを接続して情報取得
    fetch(url)
      // 成功ルート
      .then((res: Response) => {
        // resがResponse型なのは自動型推論されるので、省略可(今回は練習なので記述)
        // HTTPエラーコード(4xx/5xx)も Promise は成功とみなすため、チェックを追加
        // HTTPエラーコードで返ってきたときの処理
        if (!res.ok) {
          throw new Error(`HTTP Error: ${res.status}`); // Error型の新規オブジェクトとして生成→errorもError型
        }
        return res.json(); //res: fetchで受け取ったデータを格納した変数⇒json形式に変換(data)
      })
      // dataはjson形式かつresolveとして返されるので、Promiseと同じ結果→PokemonListResponse型
      .then((data: PokemonListResponse) => resolve(data)) // dataを「成功」として返す(resolve関数使用)【成功ルート完了】
      // ここから失敗ルート
      .catch((error: Error) => {
        // errorの型はTypeErrorになることもあるので、anyか自動推論に任せてもOK
        // fetchや res.json() で発生したエラーを Promise の失敗ルートに送る
        reject(error);
      });
  });
};

所感・小括

  • 教材なので必要なところだけ記述してるのだろうけど、エラー変数が定型としてある以上ちゃんと処理がないと腑に落ちない
  • error の処理もいろいろある。”3”参照
  • 学び:何が起きてもいいように安全なコードを意識・実践

3. 関数を専用ファイルに切り出し

教材の元コード

  • js 記法
  • App()内に具体的な処理コード(loadPokemon())を記述している
/* App.js */

import { useEffect } from 'react';
function App() {
  // 土台になるポケモンAPIのURLを指定
  const initialURL = 'https://pokeapi.co/api/v2/pokemon';

  useEffect(() => {
    const fetchPokemonData = async () => {
      // 全ポケモンのデータを取得する処理
      const resPokemon = await getAllPokemon(initialURL);

      // 各ポケモンの個別詳細データを取得
      loadPokemon(resPokemon.results);
    };
  }, []);

  const loadPokemon = (data) => {
    let _pokemonData = Promise.all(
      data.map((pokemon) => {
        let pokemonRecord = getPokemon(pokemon.url);
      })
    );
  };

  return <div className="App">{loadng ? <h1>loadng now</h1> : <h1>Pokemon!</h1>}</div>;
}

export default App;

やったこと 1

  • App.tsx は処理の流れだけ書くようにしたい
    • loadPokemon()の詳細は関数用のファイルに切り出したい(責務分離)
    • 細かい処理は処理用関数を記述してるpokemon.tsxに切り出そう!
  • loadPokemon()内部で呼び出しているgetPokemon()が fetch を使用している
    • try/catch構文でgetPokemon()がエラーで返ってきた場合の処理を追加
    • 返ってきたエラーの内容次第で型が変わるのでcatch(error)には敢えて型をつけない
    • エラーの型に応じて処理を分岐させることでより型安全に実行可能
    • ※エラーが戻ってくるならエラー対応させた方がいいのか?と AI に訊いたら YES と返ってきたので詳細な書き方は AI 産

変更後のコード1

  • loadPokemon()を関数ファイルにで記述
/* pokemon.tsx */

/* パーツとして使用する関数を記述 */
import type { PokemonResult, PokemonDetail } from './types'; // ユーザー定義型を読み込む(type{型})

/*** @name loadPokemon
 *   @function
 *   @type PokemonDetail[]
 *   @param data:PokemonResult[]
 *   @return PokemonDetail[]
 *  各ポケモンデータの配列から、ULR部分を取り出す
 */
// loadPokemonの詳細
export const loadPokemon = async (data: PokemonResult[]): Promise<PokemonDetail[]> => {
  try {
    // 内部変数_pokemonDataを定義
    // Promise.all()内部の処理がすべて終わったら結果をpokemonDataに格納
    const _pokemonData = await Promise.all(
      // 引数で受け取ったdata[]に対し、mapで同じ処理を全配列に行う
      // 配列の個々のデータ名を pokemon と定義
      data.map((pokemon: PokemonResult) => {
        // console.log(pokemon);
        const pokemonRecord: Promise<PokemonDetail> = getPokemon(pokemon.url);
        return pokemonRecord; //結果は_pokemonDataに格納される
      })
    );
    return _pokemonData; // 結果を戻す
  } catch (error) {
    // 型ガード⇒errorが Error オブジェクトであることを確認する
    if (error instanceof Error) {
      // Error型確定⇒安全に message にアクセス可能
      console.error('loadPokemon()においてデータ取得中にエラーが発生しました。処理を停止します:', error.message);
      alert('エラーが発生しました。詳細はコンソールログを確認してください');
      // ※ loadPokemon の役割は「データをロードして終わり」なので
      // コンソール出力だけで処理を止める⇒throwしない
    } else {
      // 予期せぬ、Errorオブジェクトではない何かが飛んできた場合
      console.error('loadPokemon()において予期せぬエラーが発生しました:', error);
    }
    // 失敗時: 呼び出し元に空の配列を返し、「データはゼロ件だった」と伝える
    // ※戻り値の型がオブジェクトの配列型のため
    return [];
  }
  // index.tsxに出力する部分
  return <div className="App">{loadng ? <h1>loadng now</h1> : <h1>Pokemon!</h1>>}</div>;
  export default App;
};

やったこと 2

  • pokemon.tsx切り出したloadPokemon()結果をApp.tsx受け取る必要がある
  • loadPokemon()関数を呼び出したuseEffect()スコープの外(return())で、loadPokemon()の戻り値を使いたい
    • 変数は極力 let ではなく const にしておきたい
    • useStateなら 外スコープ外で const で宣言した上で、変数の中身を変更できる!
    • こういう局面で useState を使うことを理解

変更後のコード 2

  • 呼び出しスコープ外で戻り値を使用できるようにuseState()で変数管理
/* App.tsx */
import { useEffect, useState } from 'react';
import type { PokemonListResponse, PokemonDetail } from './utilities/types';
import { getAllPokemon, loadPokemon } from './utilities/pokemon'; // getAllPokemon関数を呼び出し

function App() {
  // 各ポケモンの詳細情報を格納(useEffect外で使用)
  const [pokemonDetailData, setPokemonDetailData] = useState<PokemonDetail[]>([]);

  useEffect(() => {
    const fetchPokemonData = async (): Promise<void> => {
      // loadPokemon()がエラーで返ってくるケースを考慮してtry/catch構文使用
      try {
        // 略
        const resLoadPokemon: PokemonDetail[] = await loadPokemon(resPokemon.results);
        // このスコープ外で使いたい値をuseStateで管理している変数に格納
        setPokemonDetailData(resLoadPokemon);
      } catch (error) {
        // awaitの処理が失敗(reject)されたらこっちに入る(引数:error)
        console.error('fetchPokemonData()においてデータ取得中にエラーが発生しました:', error);
      }
    };
    fetchPokemonData(); // 定義した全体処理の関数を実行
  }, []);
}
  // index.tsxに出力する部分
  return <div className="App">{
    loadng ?
    <h1>loadng now</h1> :
    <div>{pokemonDetailData.map()}</div>  //useStateで定義した変数を使用
    }</div>;
  export default App;
};

所感・小括

  • 切り出した方が保守性が上がる

  • やはり、そもそもメインスキームに呼び出す関数の中身まで書くと美しくない

  • 呼び出し時の処理がひと手間増えるが、保守性と可読性を上げるためなら長期的に見て必要コストと認識

  • 学び:ファイルとコードの役割に応じた責務分離は大事

4. 非同期処理

元のコード

  • 教材準拠で書いた ts
  • オーソドックスな Promise()⇔then 祭
/* pokemon.tsx */

/* パーツとして使用する関数を記述 */
import type { PokemonDetail } from './types'; // ユーザー定義型を読み込む(type{型})

export const getPokemon = (url: string) => {
  // APIで取得した情報を戻す
  return new Promise<PokemonDetail>((resolve, reject) => {
    // fetchで引数のURLに対しAPIを接続して情報取得
    fetch(url)
      // 成功ルート
      .then((res: Response) => {
        // resがResponse型なのは自動型推論されるので、省略可(今回は練習なので記述)
        // HTTPエラーコード(4xx/5xx)も Promise は成功とみなすため、チェックを追加
        // HTTPエラーコードで返ってきたときの処理
        if (!res.ok) {
          throw new Error(`HTTP Error: ${res.status}`); // Error型の新規オブジェクトとして生成→errorもError型
        }
        return res.json(); //res: fetchで受け取ったデータを格納した変数⇒json形式に変換(data)
      })
      // dataはjson形式かつresolveとして返されるので、Promiseと同じ結果→PokemonDetail型
      .then((data: PokemonDetail) => {
        resolve(data);
      }) // dataを「成功」として返す(resolve関数使用)【成功ルート完了】
      // ここから失敗ルート
      .catch((error: Error) => {
        // errorの型はTypeErrorになることもあるので、anyか自動推論に任せてもOK
        // fetchや res.json() で発生したエラーを Promise の失敗ルートに送る
        reject(error);
      });
  });
};

やったこと

  • 関数の戻り値の型アノテーション設定中に、質問攻めにしていた AI が非同期処理の方法おいてPromise以外の構文を提案
/* AIが提示した例文 */

// 修正後の useEffect スコープ内
  useEffect(() => {
    // 非同期処理でAPIから情報取得処理を定義
    const fetchPokemonData = async (): Promise<PokemonDetail[]> => {
        // ... (省略: try/catchブロックの中身) ...

        // ★修正1: loadPokemonの結果は PokemonDetail[] です ★
        const resLoadPokemon: PokemonDetail[] = await loadPokemon(resPokemon.results);
        return resLoadPokemon;

        // ★注意: return されると、この行には到達しないため、ここに set を書くのは間違いです。

      } catch (error) {
            // ... (catchブロックの中身) ...
        return [];
      };

    // ★修正2: async 関数を定義し、それをすぐに実行する(IIFEパターン)★
    const startFetching = async () => {
        const resultData = await fetchPokemonData(); // ★ await で Promise の中身を取り出す!

        // 中身が取り出された(Promiseでなくなった)配列を渡す
        setPokemonDetailData(resultData);

        // データをセットしてから、ローディングを終了する
        setLoading(false);
    };

    startFetching();

  }, [initialURL]); // initialURLが外のスコープから来ているため、依存配列に追加するのが安全です

// ... (関数の定義終わり) ...
  • Promise使わなくても非同期処理ってできるんだ!?
  • async / await、見かけたことはあったが理解が浅かったのでこの機会に深堀り
  • Promise then.より await で処理した方がすっきりする……?
  • async / awaitのがより明確な書き方と調べがついたので、 Promise からリライト
  • ついでにエラーハンドリングも2. コードの補完の記法にリライト

変更後のコード

/* pokemon.tsx */

import type { PokemonDetail } from './types'; // ユーザー定義型を読み込む(type{型})

/*** @name getPokemon
 *   @function
 *   @type PokemonDetail
 *   @param url:string
 *   @return Promise<PokemonDetail>
 *  引数で受け取ったurlをAPIとして、各ポケモンの詳細情報を取得する
 *  戻り値の型を明示し、async/awaitでより簡潔に処理する
 */
export const getPokemon = async (url: string): Promise<PokemonDetail> => {
  // try/catchで処理
  try {
    // fetch(url)で通信が終わるのを待って(await)、resに格納
    const res: Response = await fetch(url);

    // HTTPエラーコード(4xx/5xx)も Promise は成功とみなすため、チェックを追加
    // HTTPエラーコードで返ってきたときの処理
    if (!res.ok) {
      throw new Error(`HTTP Error: ${res.status}`); // Error型の新規オブジェクトとして生成→errorもError型
    }

    // res.JSON()⇒ json変換
    // await res.json()⇒json化が全部終わるのを待つ
    return (await res.json()) as PokemonDetail;
    // 成功処理ここまで
  } catch (error) {
    // error の型は unknown に推論される

    // 投げられたものが本当に Error オブジェクトであるかを確認する
    if (error instanceof Error) {
      // ここでは error が安全に Error 型として扱える
      console.error('getPokemon()内:Fetchエラー:', error.message);
      console.error('スタックトレース:', error.stack);

      throw error; // throw するエラーも Error 型であることが保証される
    } else {
      // Error オブジェクト以外の何かが投げられた場合
      console.error('getPokemon()内で予期せぬエラーが発生しました:', error);
      // unknown のまま throw するか、新しい Error にラップして throw します
      throw new Error(`予期せぬエラー: ${String(error)}`);
    }
  }
};

非同期処理の流れ

  • getPokemonPromise/then 版と async/await 版の振る舞いを sequence 図で表現
  • ※AI 生成
Promise 版
  • new Promise()fetch().then().then().catch() の流れを追う
  • resolve/reject が明示的に見える ⇒ 並列処理や細かい成功/失敗分岐の可視化に向いている
  • ネストが深くなると可読性が下がる
sync/await 版
  • await を使った逐次的な読みやすさが特徴
  • try/catch でエラーハンドリングをまとめやすく、リファクタや保守がしやすい

所感・小括

  • Promise/thenは短文ならいいが長文だと読解が大変

    • 既存コードを読解・修正できる知識は必要
  • 学びasync / awaitの方が記述も読解も手軽さが UP⇒ 積極的に使いたい

5. 型エイリアスと interface

やったこと

所感・小括

  • 現時点ではinterfaceを使用して型定義を行っているが、オブジェクト型の戻り値なので比較的適切な採用と判断
  • 型エイリアスの方が少し採用率が高そう
  • と思っていたら新規論説も出ていた

  • 学び:実際の現場ではチームの方針優先
    • パフォーマンスを考慮する場合はチーム内で使い方を統一が大事

6. 新たなエラーハンドリング

経緯

  • エラーの自力調査工程を増やす
    • 「いっそ ts のエラー番号と内容の対応表一覧を作るか?」と思い立つ
  • 一覧を作っている先人がいないか「ts エラー」で調査
    • ts-resultの情報と遭遇・精査
    • 型管理の点からもtry/catchよりts-resultの方が良いのでは?
    • ts-resultについて調べていたらneverthrowにもたどり着いた
  • どちらにせよポートフォリオかつ「修作」
    • 便利そうなライブラリなどはとりあえずキャッチアップしてみる
    • 「比較的新しい方のneverthrow使ってみよ」
  • 現在の環境でも問題なく使えそうなのでリライトしてみる
  • neverthrowについて深堀りした記事はこちら ↓

  • 導入してみた
    • package.jsonと同じディレクトリでインストールコマンド実行
      • $ npm install ts-results
    • 安全に利用するために専用の ESLint プラグインも導入
      • $ npm install eslint-plugin-neverthrow
/* package.json */
{
  "dependencies": {
    "eslint-plugin-neverthrow": "^1.1.4",
    "neverthrow": "^8.2.0",
    "react": "^19.1.1",
    "react-dom": "^19.1.1"
  }
}
  • 導入成功

元コード

  • try/catch構文
const getPokemon = async (url: string): Promise<PokemonDetail> => {
  // try/catchで処理
  try {
    // fetch(url)で通信が終わるのを待って(await)、resに格納
    const res: Response = await fetch(url);

    // HTTPエラーコード(4xx/5xx)も Promise は成功とみなすため、チェックを追加
    // HTTPエラーコードで返ってきたときの処理
    if (!res.ok) {
      throw new Error(`HTTP Error: ${res.status}`); // Error型の新規オブジェクトとして生成→errorもError型
    }

    // res.JSON()⇒ json変換
    // await res.json()⇒json化が全部終わるのを待つ
    return (await res.json()) as PokemonDetail;
    // 成功処理ここまで
  } catch (error) {
    // error の型は unknown に推論される

    // 投げられたものが本当に Error オブジェクトであるかを確認する
    if (error instanceof Error) {
      // ここでは error が安全に Error 型として扱える
      console.error('getPokemon()内:Fetchエラー:', error.message);
      console.error('スタックトレース:', error.stack);

      throw error; // throw するエラーも Error 型であることが保証される
    } else {
      // Error オブジェクト以外の何かが投げられた場合
      console.error('getPokemon()内で予期せぬエラーが発生しました:', error);
      // unknown のまま throw するか、新しい Error にラップして throw します
      throw new Error(`予期せぬエラー: ${String(error)}`);
    }
  }
};

変更後のコード

  • neverthrow ライブラリを利用
import { ResultAsync, fromPromise, err } from 'neverthrow'; // neverthrowライブラリを読み込み

// neverthrow で使用するエラー型(実際はtypes.tsxに集約)
export interface FetchError {
  type: 'HTTP_ERROR' | 'NETWORK_ERROR' | 'PARSE_ERROR';
  message: string;
  status?: number; // HTTPエラーの場合のみ
}

// async処理の代わりに戻り値の型がResultAsync<>になる
export const getPokemon = (url: string): ResultAsync<PokemonDetail, FetchError> => {
  // fetch処理を「大声(例外)を出す Promise」として neverthrowのメソッド・fromPromise でラップ
  // fromPromiseはneverthrowのメソッド・「ResultAsyncの箱」に変身させる道具なので、戻り値の型はPromise型かつResultAsync<成功,失敗>
  // 成功:Response≒fetchのresolve
  // 失敗:FetchError≒fetchのreject
  const fetchPokemonDetail: ResultAsync<Response, FetchError> = fromPromise(
    fetch(url), // fetchは成功時にPromise<Response>を返す

    // ネットワーク接続失敗処理
    (error: unknown): FetchError => ({
      // errorは暗黙的にunknownと推察される
      // ❌ Promiseが reject (ネットワークエラーなど) されたとき、
      //    その例外(Error)を Err の失敗報告書(FetchError)に変換する
      type: 'NETWORK_ERROR',
      message: `ネットワーク接続に失敗: ${(error as Error).message}`,
      // ここではerrorがunknownのままだとプロパティが使用できない
      // ⇒型アサーションでError型に定義
    })
    // ネットワーク接続失敗処理ここまで
  );
  // andThen⇒Responseが成功(Ok)した場合の次の処理を継続する
  return (
    fetchPokemonDetail
      .andThen((resFetch: Response) => {
        // fetch成功時のResponseオブジェクトを変数resFetchに格納
        // Response型には要素としてok,statusが含まれている

        // HTTPステータスコードのチェック⇒従来のtry内のステータス確認の工程
        if (!resFetch.ok) {
          // res.okがfalseの場合、throwではなく Err の箱を返す
          const httpError: FetchError = {
            type: 'HTTP_ERROR',
            message: `HTTPエラーが発生: ${res.status}`,
            status: resFetch.status,
          };
          // ResultAsync内で失敗報告書を返す
          return err(httpError); //err:neverthrowのメソッド
        }
        // HTTPステータスコードのチェックここまで

        // JSON解析処理も fromPromise で安全にラップする
        return fromPromise(
          resFetch.json() as Promise<PokemonDetail>,
          // JSON解析エラーが発生した場合、それを失敗報告書(変数error)に変換
          (error): FetchError => ({
            type: 'PARSE_ERROR',
            message: `JSON解析エラー: ${(error as Error).message}`,
          })
        );
        // JSON解析のエラー処理ここまで
      })
      // fetchPokemonDetailの処理が終わったらmapで仕上げ処理
      // PokemonDetail型に変換したjson()を変数pokemonDetailDataに入れる
      .map((pokemonDetailData: PokemonDetail) => {
        // jsonを返す
        return pokemonDetailData;
      })
  );
};

所感・小括

  • コードは長くなるが、エラーごとに詳細残せるメリットが大きい
  • 非同期処理の再学習からやり直すことになったので大変だった (参照
  • 学び:堅牢なガード、大事

7. 共通処理の切り出し

  • 関数の開始箇所付近のエディタにComplexity is 10 It's time to do something...と表示されていた
  • どうもコードが複雑化していたもよう
    • VScode 拡張機能のCodeMetricsによる算出 -非同期処理におけるエラー処理などが複数の関数で使用されていたので共通処理として別関数に切り出すことにした

教材の非同期処理の元コード

※もはや教材動画の記述から離れた箇所に手を加えている

/* getAllPokemon() */
const getAllPokemon = (url: string): ResultAsync<PokemonListResponse, FetchError> => {
  const fetchPokemonAll: ResultAsync<Response, FetchError> = fromPromise(
    fetch(url), // fetchは成功時にPromise<Response>を返す
    (error: unknown): FetchError => ({
      type: 'NETWORK_ERROR',
      message: `ネットワーク接続に失敗: ${(error as Error).message}`,
    }),
  );
  // ★共通箇所↓
  return (
    fetchPokemonAll
      // 処理1:HTTPエラー処理
      .andThen((resFetch: Response) => {
        if (!resFetch.ok) {
          const httpError: FetchError = {
            type: 'HTTP_ERROR',
            message: `HTTPエラーが発生: ${resFetch.status}`,
            status: resFetch.status,
          };
          return err(httpError); //err:neverthrowのメソッド
        }
        // 処理2:JSON化
        return fromPromise(
          resFetch.json() as Promise<PokemonListResponse>,
          (error): FetchError => ({
            type: 'PARSE_ERROR',
            message: `JSON解析エラー: ${(error as Error).message}`,
          }),
        );
      })
      // ★共通箇所↑
      .map((pokemonAllData: PokemonListResponse) => {
        return pokemonAllData;
      })
  );
};

/* getPokemon() */
const getPokemon = (url: string): ResultAsync<PokemonDetail, FetchError> => {
  const fetchPokemonDetail: ResultAsync<Response, FetchError> = fromPromise(
    fetch(url), // fetchは成功時にPromise<Response>を返す
    (error: unknown): FetchError => ({
      type: 'NETWORK_ERROR',
      message: `ネットワーク接続に失敗: ${(error as Error).message}`,
    }),
  );
  // ★共通箇所↓
  return (
    fetchPokemonDetail
      // 処理1:HTTPエラー処理
      .andThen((resFetch: Response) => {
        if (!resFetch.ok) {
          const httpError: FetchError = {
            type: 'HTTP_ERROR',
            message: `HTTPエラーが発生: ${resFetch.status}`,
            status: resFetch.status,
          };
          return err(httpError); //err:neverthrowのメソッド
        }
        // 処理2:JSON化
        return fromPromise(
          resFetch.json() as Promise<PokemonDetail>,
          (error): FetchError => ({
            type: 'PARSE_ERROR',
            message: `JSON解析エラー: ${(error as Error).message}`,
          }),
        );
      })
      // ★共通箇所↑
      .map((pokemonDetailData: PokemonDetail) => {
        return pokemonDetailData;
      })
  );
};

やったこと

  • getAllPokemon()getPokemon()において前述の 2 処理が共通
    • fetch 処理における HTTP エラー処理
    • JSON 変換の処理
  • 共通化して関数として切り出す

変更後のコード

  • 簡約
/* 共通関数 */
/*** @name checkResponseAndParseJson
 *   HTTPステータスチェックとJSON解析(非同期)の共通化
 *   @function
 *   @type ResultAsync<T, FetchError>
 *   @param resFetch: Response
 *   @return ResultAsync<T, FetchError>
 *   成功時の型 T はジェネリクスで指定する。
 */

const checkResponseAndParseJson = <T,>(resFetch: Response): ResultAsync<T, FetchError> => {
  if (!resFetch.ok) {
    // res.okがfalseの場合、throwではなく Err の箱を返す
    const httpError: FetchError = {
      type: 'HTTP_ERROR',
      message: `HTTPエラーが発生: ${resFetch.status}`,
      status: resFetch.status,
    };
    // 失敗報告書ををResultAsyncに変換して返す
    // ⇒呼び出し元のandThen内で型を合わせるため
    return new ResultAsync(Promise.resolve(err(httpError))); //err:neverthrowのメソッド
    // ※同期的な Result を非同期の ResultAsync に変換する
    // 1. err(httpError): 確定した同期的な失敗報告書(Result)を作成。
    // 2. Promise.resolve(...): その報告書を「即座に成功する Promise」でラップ。
    // 3. new ResultAsync(...): 「結果が確定している Promise<Result>」を、ResultAsync の箱として構築
  }

  return fromPromise(
    // Tは呼び出し元が期待する成功時の型(PokemonListResponseまたはPokemonDetail)
    resFetch.json() as Promise<T>,
    // JSON解析エラーが発生した場合、それを失敗報告書(変数error)に変換
    (error): FetchError => ({
      type: 'PARSE_ERROR',
      message: `JSON解析エラー: ${(error as Error).message}`,
    })
  );
};

/* 呼び出し */
const getAllPokemon = (url: string): ResultAsync<PokemonListResponse, FetchError> => {
  const fetchPokemonAll: ResultAsync<Response, FetchError> = fromPromise(
    fetch(url), // fetchは成功時にPromise<Response>を返す
    (error: unknown): FetchError => ({
      type: 'NETWORK_ERROR',
      message: `ネットワーク接続に失敗: ${(error as Error).message}`,
    })
  );
  return fetchPokemonAll
    .andThen((resFetch: Response) => {
      // ★HTTPエラー処理とJSON変換処理を関数で実行
      return checkResponseAndParseJson<PokemonListResponse>(resFetch);
    })
    .map((pokemonAllData: PokemonListResponse) => {
      return pokemonAllData;
    });
};

const getPokemon = (url: string): ResultAsync<PokemonDetail, FetchError> => {
  const fetchPokemonDetail: ResultAsync<Response, FetchError> = fromPromise(
    fetch(url), // fetchは成功時にPromise<Response>を返す
    (error: unknown): FetchError => ({
      type: 'NETWORK_ERROR',
      message: `ネットワーク接続に失敗: ${(error as Error).message}`,
    })
  );
  return fetchPokemonDetail
      .andThen((resFetch: Response) => {
        // ★HTTPエラー処理とJSON変換処理を関数で実行
        return checkResponseAndParseJson<PokemonDetail>(resFetch);
      })
      .map((pokemonDetailData: PokemonDetail) => {
        return pokemonDetailData;
      })
  );
};

所感・小括

  • 関数に切り出した結果、複雑度が下がった
  • 上記の処理のあと、fetch で API 情報を取得する箇所も重複しているので共通化
  • 学び:共通化すると複雑度が下がる・可読性が増す ⇒ メンテが楽になる!

8. リファクタリング(コードの簡略化)

教材の非同期処理の元コード

※もはや教材動画の記述から離れた箇所に手を加えている

  • 共通部品を関数として切り出してもまだ複雑度が高い
    • 既に確定している処理を改めて実行している箇所を整理
  • 複雑度を更に下げる
/* 確定処理を整理 */
/* getPokemon() */
const getPokemon = (url: string): ResultAsync<PokemonDetail, FetchError> => {
  const fetchPokemonDetail: ResultAsync<Response, FetchError> = fetchWrapper(url);
  // andThen⇒Responseが成功(Ok)した場合の次の処理を継続する
  // ⇒処理を連続されられる
  return (
    fetchPokemonDetail
      .andThen((resFetch: Response) => {
        return checkResponseAndParseJson<PokemonDetail>(resFetch);
      })
      // mapで仕上げをしなくても、引数をreturnしているだけなのでcheckResponseAndParseJsonの結果を直接返して簡略化
      .map((pokemonDetailData: PokemonDetail) => {
        return pokemonDetailData;
      })
  );
};

変更後のコード

/* getPokemon() */
const getPokemon = (url: string): ResultAsync<PokemonDetail, FetchError> => {
  return (
    // fetchを含む処理:fetchWrapper使用
    fetchWrapper(url)
      // fetch成功時のResponseオブジェクトを変数resFetchに格納
      // 成功時のみ続けて処理
      //    失敗時はエラーに格納されてここで戻る
      .andThen((resFetch: Response) => {
        // HTTPエラー処理とJSON変換処理を関数で実行
        // 成功結果がPokemonListResponse型JSON
        // ⇒.mapを嚙まさずに直接戻す
        // 失敗でもFetchError型の結果が戻る
        return checkResponseAndParseJson<PokemonDetail>(resFetch);
      })
  );
};

所感・小括

  • 簡略化しようと思えばかなり簡略可能、というより「処理を圧縮」してる感覚
    • 魔法とか技の「詠唱破棄」してる気分 ⇒ 高難易度という証左か?
  • 自力での簡略化は難しく、AI に「こうできる?」と訊いて記述してもらうのが現状限界
    • AI を下請けとして活用
    • 大部分を AI に頼ったので「なぜそのコードになるのか」を問い詰めて分解・整理してコメントに残した
  • リファクタリング時に型が変わることも少なくなかった
    • 発生したエラーの検索 ⇒ 概念を把握
    • エラー解消に適用する際の実際の考え方を AI に聞きながら無限 1on1 で修正
  • 可読性は上がるが、コメントを残しておかないとメンテ者が「解読」に無限に時間を吸われかねない(特に新人)
  • 学び:経験値を積んでからじゃないと使いこなせない武器。が、使いこなせれば強力。

9. Sass と Vite

疑問発生

  1. Sass の勉強をした際

    • scss ファイルがコンパイルされると css として出力される
    • vite 環境で dev 実行 ⇒css ファイルが出力されていない??
  2. スタイルファイルを読み込むコード

    • css ファイルではなく、コンパイル後の**.scssが直接指定されている!?
  • 疑問解消のため検索するも、ピンとくる技術記事がなかったので AI に訊いた

調査結果

  1. vite の仕様で開発時の dev 環境では css ファイルは出力されない
    • vite の強みの一つは開発時のコンパイル高速化
    • その一環で css ファイルは物理保管されずメモリ上に保存されるだけ
    • どうしても css ファイルを出力したいなら Vite のwriteBundleフックを利用すれば可能
  2. scss を直接指定できるのが vite
    • そもそも tsx 自体がコンパイルされ js になる
    • よって tsx に scss が読み込む ⇒scss もコンパイル時に自動で css に変換作業が行われる
    • vite がコンパイル関連の処理をまとめてやってくれてる!

所感・小括

  • コンパイルやコンパイル後のファイル含めて vite がほぼまとめて処理してくれる
  • ⇒ 従来の React や Sass の書き方のまま使用可能
  • 学び:vite の動作思想:開発速度を上げるために「出力」を減らすこと
    • vite が複雑な設定・工程を裏で実行 ⇒ 開発者:記述・設定のコスト減
    • レガシー動作を知った上で、技術進化で減少した手間とそのギャップを把握することも重要(開発・保守どちらにも転用可能)

10. 取得情報の追加

教材の元コード

  • ブラウザロード時に
    1. 全体のデータを fech で取得
    2. 1 のレスポンス内容を基に詳細のデータを取得
      の 2 段構えのコード
  • コードをシンプルにするために 1,2 の処理を、教材から以下のようにリファクタリングしていた
export const fetchPokemonData = (initialURL: string): ResultAsync<PokemonDetail[], FetchError> => {
  // 1.全ポケデータを取得
  // andThenでチェーンで以下の処理を繋ぎ、すべて終わったら親関数に戻す
  //  1. getAllPokemon(initialURL)でポケモン全情報を取得
  //  2. 成功したらgetAllPokemon()の成功結果を使ってloadPokemon()実行
  //  3. loadPokemon()の戻り値がgetAllPokemon()に届く
  //  4. getAllPokemon()に届いた値をfetchPokemonData()の戻り値としてreturnする
  // 最終的に ResultAsync型 で戻る
  return (
    getAllPokemon(initialURL) // src/utilities/pokemon.tsxの関数にAPIのUPLを渡す
      // 成功したら andThen で次の処理
      //  成功結果を変数resAllPokemonに格納して処理
      .andThen((resAllPokemon) => {
        // loadPokemon()が成功⇒結果をgetPokemonInfoに格納
        return loadPokemon(resAllPokemon.results);
      })
  );
};
  • getAllPokemon()の結果(resAllPokemon)から、特定の要素も追加して return する必要が発生

やったこと

  • neverthrowライブラリを使用:機能を活用し戻り値への要素の追加を.map( (引数) => {処理} )で実行
  • 戻り値の型の修正
    • PokemonDetail[]型を返していたが、必要な要素を追加した型PokemonDetailAndURLを再定義
  • ※型を再定義と「要素を追加して返す」とことまでは自力で思いついたが、return 文の分割など、手法で彷徨った
    • 実際の記述方法には辿り着けなかったので AI に訊いた

変更後のコード

export const fetchPokemonData = (initialURL: string): ResultAsync<PokemonDetailAndURL, FetchError> => {
  // 1.全ポケデータを取得
  // andThenでチェーンで以下の処理を繋ぎ、すべて終わったら親関数に戻す
  //  1. getAllPokemon(initialURL)でポケモン全情報を取得
  //  2. 成功したらgetAllPokemon()の成功結果を使ってloadPokemon()実行
  //  3. loadPokemon()の戻り値に、getAllPokemonのprevious,nextプロパティも追加する ←★変更箇所
  //  4. 3の結果がgetAllPokemon()に届く
  //  5. getAllPokemon()に届いた値をfetchPokemonData()の戻り値としてreturnする
  // 最終的に ResultAsync型 で戻る
  return (
    getAllPokemon(initialURL) // src/utilities/pokemon.tsxの関数にAPIのUPLを渡す
      // 成功したら andThen で次の処理
      //  成功結果を変数resAllPokemonに格納して処理
      .andThen((resAllPokemon) => {
        // loadPokemon()が成功⇒結果をgetPokemonInfoに格納
        console.log(resAllPokemon);
        // 個別のポケモンデータを格納
        return (
          loadPokemon(resAllPokemon.results)
            // loadPokemonの結果をsuccessLoadPokemon:PokemonDetail[]とする
            // 前後20匹ずつのURL情報も一緒に格納するために、neverthrowの.mapで加工
            // オブジェクトを直接返す⇒関数と区別するように{}を()で囲む
            .map((successLoadPokemon: PokemonDetail[]) => ({
              pokemonDetailData: successLoadPokemon,
              previous: resAllPokemon.previous,
              next: resAllPokemon.next,
            }))
        );
      })
  );
};

問題が追加発生

  • 必要な要素を追加した型を再定義・戻り値型として設定したが、return 文で型の不一致が発生
  • 原因:再定義した型の定義が、基となるresAllPokemonの定義と異なっていた

誤ったコード

export interface PokemonListResponse {
  count: number; // ポケモンデータの総数
  next: string | null; // 次のページへのURL (文字列、または最終ページの場合は null)
  previous: string | null; // 前のページへのURL (文字列、または最初のページの場合は null)
  results: PokemonResult[]; // ポケモン情報のリスト(上記で定義した PokemonResult 型の配列)
}

export interface PokemonDetailAndURL {
  next: string;
  previous: string;
  pokemonDetailData: PokemonDetail[];
}
  • nextpreviousに null 許可がない ⇒ エラーの原因になった

修正後のコード

  • 手動で再定義しようとしたからエラーが出た
    • 元の型定義を再利用した方が安全
  • PokemonListResponseから必要要素を Pick した型と、元々返していた要素に名前を付けた型を結合して再定義
    • PokemonListResponse,PokemonDetailに修正が出ても、自動で反映される!
export type PokemonDetailAndURL = Pick<PokemonListResponse, 'next' | 'previous'> & { pokemonDetailData: PokemonDetail[] };

所感・小括

  • 「やりたいこと:返すデータの追加」に対して return 文に少し追加記述するだけで返せるのは驚いた
    • たまたまライブラリの機能を活用できる場面だったという側面もある
    • 状況次第では他の処理法や、もっとシンプルに記述できるケースもあるはず
  • 手動で再定義をすると!間違える!!!
  • 学び:既存の型のデータを使用した型を再定義する際は、絶対に手動で再定義しない。
    • よほどどうしようもない限り、流用して再定義する
    • ヒューマンエラーの防止!!!

11. Github Pages での公開

トラブル

  • 公開したページが真っ白表示
  • 調べたら vite 環境だと発生しやすい
  • 類似トラブルの対応をした記事の通りに対応
  • やっぱり真っ白
  • 調べたが発生母数が少ないのか情報を見つけられなかったので AI に質問

教えて!AI 先生

  • package.jsonの中身を AI に提供して解決方法を訊く
    • package.jsonの中身を貼れば、設定やバージョンを一気に明示できる

AI:公開ページの開発者ツールのコンソールに「404 エラー出てない?」

出てる

AI:Vite がアセットのパスをルート (/assets/index.js) から探しているのに対し、実際のファイルはリポジトリ名以下 (/プロジェクトファイル/assets/index.js) にあるため、ファイルが見つからない(404 エラー)状態
AI:package.json内記載のhomepage設定 ⇒Vite はデフォルトでは使用しない

AI 指示による対処法

  • vite.config.tsの設定を変更
    • baseパス(github のリポジトリ名)を設定
    • このアプリケーションは「(Github ユーザー名)」パスの「ベース=サブディレクトリ=リポジトリ名」で公開という情報を指定
  • 設定変更後、再ビルド&デプロイ
    • 再ビルド npm run build
    • 再デプロイ npm run deploy
    • ※コマンドはpackage.json内で指定されているもの
// vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// import { resolve } from 'path';  // ★追加:エイリアスで@などを使用している場合はインポート

// ★追加:github pagesで公開する際のサブディレクトリ(リポジトリ)名
const repositoryName = '/pokemon-app/';

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    react({
      babel: {
        // reactCompilerを有効にしている箇所↓↓
        plugins: [['babel-plugin-react-compiler']],
        // reactCompilerを有効にしている箇所↑↑
      },
    }),
  ],
  // ★追加:ビルドディレクトリの設定
  // package.json > scripts > deploy に「gh-pages -d build」の設定がある場合
  // ⇒ 下記:outDirの設定必要
  build: {
    outDir: 'build', // デフォルト:'dist'
  },
  server: {
    port: 3000,
  },
});

設定変更してもキャッシュをクリアしてもまだ真っ白!

再・教えて!AI 先生

  • コンソールに出ていたエラー
    • GET ***/src/main.tsx net::ERR_ABORTED 404 (Not Found)Understand this error
    • ⇒ レアなエラー
  • 原因:アプリケーションが初期化される前に。ブランチが直にsrc/main.tsxを読もうとしている
    • ルートファイル直下のindex.htmlに開発用スクリプトが残っている
    • ⇒ ビルド後には存在しない tsx ファイルを、ブラウザが探そうとする!
    • <script type="module" src="/src/***"></script>に該当するコードを削除(or コメントアウト)
  • 念のため、ビルド結果を出力する build ディレクトリを削除して再ビルド
    • 最新の結果が反映されるようにする
  • ビルド実行後に build ディレクトリを Github の作業ブランチ・master ブランチにコミットされてるのも運用上よくないらしい
    • .gitignoreの対象に設定
  • まだ真っ白
  • よく確認したらnpm run deploy時にエラーが発生している
    • error: too many arguments. Expected 0 arguments but got 1.
***:~/reactProject/pokemon-app$ npm run deploy

> pokemon-app@0.0.0 deploy
> npm run build && gh-pages -d build -r -m "Updates --skip-ci"


> pokemon-app@0.0.0 build
> tsc -b && vite build

vite v7.1.7 building for production...
✓ 42 modules transformed.
build/index.html                   0.42 kB │ gzip:  0.27 kB
build/assets/index-C8RsbyQY.css    1.07 kB │ gzip:  0.54 kB
build/assets/index-7Pyurpw1.js   198.60 kB │ gzip: 62.90 kB
✓ built in 2.59s
error: too many arguments. Expected 0 arguments but got 1.
  • いろいろ試した結果、gh-pagesブランチがoriginのリモートブランチを探しに行ってしまうことが判明
    • package.json内を修正``に修正
      • 修正前:"deploy": "npm run build && gh-pages -d build -r -m \"Updates --skip-ci\""
      • 修正後:"deploy": "npm run build && gh-pages -d build --remote リモート名 --message \"Deploy Updates [skip ci]\""
    • コミットメッセージ部分:オプションの略記をしない+[]を使用して誤作動防止
    • --remote リモート名で接続先を明示
  • 再度ビルド&デプロイを実行
  • 表示成功!!!!

所感・小括

  • vite 環境だから記事で下調べしていた範囲のエラーは予期していたが、想定以上のエラー祭りだった
  • GithubPages 利用におけるブランチの仕様を知らなかった
    • 余計なプッシュをしてしまっていたので、ここで知れてよかった
  • ターミナルで表示されてるエラーの見落としがあった
    • ちゃんと読もう
  • 学び:単なるデプロイ作業ではなく、原因の層を順番に剥がしながら問題を特定したプロセス自体が学びとなった。
    特に GitHub Pages 特有の挙動(gh-pages)や、Vite のパス解決の仕組み、開発用コードが本番に残るリスク、リモート設定の落とし穴など、実務で遭遇しやすい“環境依存トラブル”を含め調査・検証・解消できたことが有意義な体験だった。

総括

  • 教材そのものの難易度は高くなかったが、js を ts で書こうとするだけで考慮する対象が一気に増えた
    • 型安全だけでなく、それに伴う機能の使い方を体得する困難さを体感
  • クリーンコードとは言うが、最低限自分でざっと読んでざっと内容を把握できる程度には読みやすいコードがいい
    • 部品の切り出しや、共通化できる処理がないか設計の時点から考慮することが大事
  • エラーが起きたら必ずどこかにエラーメッセージがある
    • 最後に発見できれば何とかなる
    • 見つけるのが早いほど解決も当然早いので注意して読む
    • 経験によるところも大きいスキルと痛感
  • なにより、記事に起こしながら完走したことに意義があると思いたい
  • 今後:まだ改造の余地がある教材 ⇒ 方針を決めて改造までして自分の中の一旦の「完成」までやりきる予定

教材準拠の成果物

参考資料

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?