💻 TypeScript でレガシーな API 地獄を終わらせる!any 排除への戦い

コード品質と TypeScript

導入:長年運用したシステムへの感謝と決断

長年 PHP 独自 MVC で Web サイトを運用してきた経験から、私は**「型」の重要性を痛感しています。特に、フロントエンド(Next.js)とバックエンド(Django)を API 経由で接続するモダンな開発に移行した際、古いシステム設計が引き起こす「見えないバグ」**に直面しました。

それが、JavaScript の世界で多発する**「any 地獄」**です。

API から送られてくるデータが「実際には何型なのか?」が不明確なまま開発を進めると、開発効率は劇的に低下し、後々致命的なバグの温床になります。本記事では、この**「レガシー API 地獄」からの脱却を目指した、私の TypeScript 導入と「any 排除への戦い」**の記録を公開します。


第1章:PHP 独自 MVC から引き継いだ技術的負債

1. データ型が曖昧なレガシー API の現実

レガシーなシステムでは、データベースからのデータ取得や API のレスポンス形式が厳密に定義されていないことが多々あります。私の旧 PHP 独自 MVC サイトも例外ではありませんでした。

  • 問題点:
    • レスポンスのフィールド名が途中で変更されても、フロント側でエラーにならない。
    • 数値(ID や Count)が文字列として返ってくることがあり、計算時に実行時エラーが発生する。
    • データが存在しない場合、null が返るのか、空の配列が返るのかが統一されていない

これらの問題は、当時の JavaScript 開発において**「このデータは何型だと信じるか?」**という属人的な判断に委ねられていました。

2. モダンな Next.js 環境への悪影響

Next.js の開発を始めたとき、これらのレガシー API をそのまま利用すると、せっかく導入した TypeScript の恩恵を全く受けられないことが判明しました。

TypeScript

// 型定義がないと、こうなってしまう...
const fetchData = async () => {
    const res = await fetch('/legacy/api/data');
    const data: any = await res.json(); // any地獄の始まり
    console.log(data.item_name.length); // item_nameがない場合、実行時エラー!
    return data;
};

開発の初期段階で data: any を多用すると、TypeScript の最大のメリットである**コンパイル時(コード記述時)**のエラーチェックが機能せず、デバッグ時間が増加するという本末転倒な事態に陥ります。


第2章:TypeScript と Zod による「厳格な門番」の設置

この課題を解決するため、私は「API レスポンスの型は、クライアントサイド(Next.js 側)で厳密に検証する」という方針を立てました。

1. TypeScript インターフェースによる期待値の明確化

まず、API が返すであろう理想のデータ構造TypeScript の Interface で定義しました。

TypeScript

// 期待するユーザーデータ構造を厳格に定義
export interface UserData {
  id: number;
  username: string;
  is_active: boolean;
  profile?: { // オプショナルな値も明確に
    bio: string;
  };
}

この Interface を定義するだけでも、開発時の補完機能(インテリセンス)が効き、作業効率が向上しますが、API がこの型に従っている保証はまだありません

2. ランタイムバリデーションライブラリ Zod の導入

ここで鍵となるのが、**ランタイム(実行時)**にデータの型を検証するライブラリである Zod の導入です。

Zod を使うことで、ネットワークから取得した JSON データが、事前に定義した TypeScript の Interface(Schema)に一致するかを実行時にチェックできます。

TypeScript

import { z } from 'zod';

// ZodでSchemaを定義 (これがInterfaceの役割を兼ねる)
const UserSchema = z.object({
  id: z.number().int(),
  username: z.string(),
  is_active: z.boolean(),
  profile: z.object({
    bio: z.string(),
  }).optional(), // 'profile' がない可能性も定義
});

// ZodのSchemaからTypeScriptの型を自動生成
export type UserData = z.infer<typeof UserSchema>; 

// API呼び出し後のデータ検証
const data = await res.json();
try {
    // ここで厳格にチェック!型が合わなければエラーを投げる
    const validatedData = UserSchema.parse(data); 
    // ここから先は、validatedData は UserData型として安全に扱える
} catch (error) {
    // ログに記録し、不正なデータは処理しない
    console.error("APIレスポンスの型が不正です", error);
}

この Zod の導入は、API とフロントエンドの間に**「厳格な門番」を設置したことを意味します。これにより、レガシー API からの不正なデータ流入を防ぎ、Next.js 側のコードから安全に any を排除**することが可能になりました。


まとめ:安全なシステム構築へ繋がる一歩

1. any 排除がもたらした効果

**「any 排除への戦い」**は、単なるコーディング規約の変更ではありませんでした。

  • 開発効率の向上: TypeScript によるコンパイル時チェックと、Zod によるランタイムチェックの二重体制により、デバッグ時間が大幅に削減されました。バグは実行時ではなく、コードを書きながら解決できるようになりました。
  • 保守性の向上: データの受け渡しに関する誤解や推測がなくなり、未来の自分やチームメンバーが安心してコードを変更できるようになりました。
  • 設計の改善: Zod で API の構造を厳格に定義する過程で、Django 側の API 設計における不備や不統一な点も明確になり、バックエンドの改善にも繋がっています。

2. 自走力が生んだ学びと展望

PHP の緩い型環境で長く開発をしてきたからこそ、私は**「型」の持つ防御力と生産性**を深く理解できました。

この課題を独力で見つけ、TypeScriptZod といったモダンなソリューションを導入・適用できた経験は、私の自律的な課題解決能力を証明するものだと自負しています。

レガシーなシステム移行は困難を伴いますが、一つ一つの技術的負債を解消していくことで、システムは堅牢になり、開発者としての自信も深まります。

次回の記事では、この API を配信するバックエンドとして採用した Django REST Framework の設計戦略について詳しく解説する予定です。

投稿者プロフィール

bicstation
AIアシスタントとの協業が、この奮闘記を可能にした
実は、今回一連の記事を執筆し、そして開発を進める上で、強力な「相棒」の存在がありました。それが、私のような開発者をサポートしてくれるAIアシスタントです。

PHPの難解なエラーログに直面した時、記事の構成がなかなか思いつかなかった時、あるいはブログのテーマに合ったアイキャッチ画像が必要だった時など、数々の場面でAIに相談し、助けを借りました。

例えば、「レンタルサーバーでのphp.ini設定の難しさ」や「.envファイルの問題」といった、私が実体験で感じた課題を伝えると、AIは瞬時にその技術的な背景や影響を整理し、ブログ記事として読者に伝わりやすい文章の骨子を提案してくれました。また、記事のテーマに合わせたアイキャッチ画像も、具体的な指示を出すだけで瞬時に生成してくれたおかげで、コンテンツ作成のスピードが格段に向上しました。

AIは完璧ではありませんが、まさに「もう一人の自分」のように、アイデアの壁打ち相手になったり、膨大な知識の中から必要な情報を引き出してくれたり、私の思考を整理する手助けをしてくれたりします。一人で抱え込みがちな開発の課題も、AIと対話することで、新たな視点や解決策が見えてくることが多々ありました。

このブログを通じて私の奮闘記を共有できているのも、AIアシスタントの存在なくしては成し得なかったでしょう。これからも、AIを賢く活用しながら、開発と情報発信を続けていきたいと思います。

\ 最新情報をチェック /

コメント

PAGE TOP
タイトルとURLをコピーしました