9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[TypeScriptシリーズ - Part 1] Conditional Types & infer

9
Posted at

Gemini_Generated_Image_vq2gzhvq2gzhvq2g-clean.png

📝 注記
私は日本語に堪能ではありません。この記事はAIのサポートを受けて執筆・翻訳されています。ご了承ください。よろしくお願いいたします。

📖 目次

  1. 問題の提示 – どんな時にこのテクニックが必要か
  2. 悪い例 – まずはダメなコードを見せる
  3. 良い例 – TypeScriptの高度機能で解決する
  4. Playgroundリンク – その場で試せる
  5. 課題 – シニア向けのチャレンジ問題
  6. まとめ

1. 問題の提示 – どんな時にこのテクニックが必要か

あなたのフロントエンドはREST APIと通信しています。全てのエンドポイントは以下のような共通のラッパー構造を返します。

interface ApiResponse<T> {
  data: T;
  meta: { page: number; total: number };
  error?: string;
}

問題点:

  • 新しいエンドポイントを追加するたびに、data の型を手動で定義しなければならない
  • バックエンドが変更される度に、フロントエンドの型定義も手動更新が必要
  • 時間の浪費、同期ミスのリスク、DRY原則の破綻

問いかけ:
どうすればTypeScriptに data の型を自動的に抽出させられるでしょうか?


2. 悪い例 – まずはダメなコードを見せる

// ❌ 毎回手動でインターフェースを定義
interface User {
  id: number;
  name: string;
  email: string;
}

interface Product {
  id: number;
  title: string;
  price: number;
}

interface Order {
  id: number;
  total: number;
  items: string[];
}

// ❌ エンドポイントごとに型を繰り返し宣言
async function fetchUser(): Promise<ApiResponse<User>> {
  return { data: { id: 1, name: "Alex", email: "alex@example.com" }, meta: { page: 1, total: 1 } };
}

async function fetchProduct(): Promise<ApiResponse<Product>> {
  return { data: { id: 1, title: "Laptop", price: 1000 }, meta: { page: 1, total: 5 } };
}

async function fetchOrder(): Promise<ApiResponse<Order>> {
  return { data: { id: 1, total: 200, items: ["item1"] }, meta: { page: 1, total: 1 } };
}

// バックエンドがUserを { id, fullName, email } に変更したら?
// → 3〜4箇所を手動で修正しなければならない

なぜ悪いのか:

問題 説明
非DRY 同じような型定義を繰り返す
エラーが起きやすい バックエンド変更時に修正漏れが発生
保守性が低い 時間が経つほど技術的負債が増える
TypeScriptの力を活かせていない コンパイラに型推論を任せられるのに

3. 良い例 – TypeScriptの高度機能で解決する

ステップ1: 関数の戻り値の型を取得する

type GetReturn<T> = T extends (...args: any[]) => infer R ? R : never;

ステップ2: ApiResponseから data を抽出する

type ExtractData<T> = T extends ApiResponse<infer U> ? U : never;

ステップ3: 組み合わせる

// ✅ 普通にfetch関数を書くだけ
async function fetchUser() {
  return {
    data: { id: 1, name: "Alex", email: "alex@example.com" },
    meta: { page: 1, total: 1 }
  };
}

// ✅ TypeScriptが自動的に型を推論
type User = ExtractData<GetReturn<typeof fetchUser>>;
// 結果: { id: number; name: string; email: string }

複数エンドポイントの例

async function fetchProduct() {
  return {
    data: { id: 1, title: "Laptop", price: 1000 },
    meta: { page: 1, total: 5 }
  };
}

type Product = ExtractData<GetReturn<typeof fetchProduct>>;
// ✅ 自動: { id: number; title: string; price: number }

// バックエンドがProductに "currency" フィールドを追加したら?
// → Product型が自動更新。何も修正する必要がない!

処理の流れ(視覚モデル)

fetchUser
    │
    ▼
GetReturn<typeof fetchUser>
    │ = Promise<{ data: User, meta: ... }>
    ▼
ExtractData<...>
    │ = T extends ApiResponse<infer U> ? U : T
    ▼
User ✅ (自動抽出完了)

さらに発展:複数のラッパー型に対応

// 別のラッパー型がある場合
type PaginatedResponse<T> = { items: T[]; total: number; page: number };

// 条件分岐で拡張可能
type ExtractDataFlexible<T> =
  T extends ApiResponse<infer U> ? U :
  T extends PaginatedResponse<infer U> ? U[] :
  T;

// 使用例
type UsersList = ExtractDataFlexible<PaginatedResponse<User>>;
// 結果: User[]

4. Playgroundリンク – その場で試せる

理論だけでは実感しにくいので、実際に動かして確認してみましょう。
TypeScript Playgroundはブラウザ上でTypeScriptを実行できる公式ツールです。インストール不要、すぐに試せます。

🔗 Playground URL: https://www.typescriptlang.org/play/

何を確認できるのか?

下のコードをコピーしてPlaygroundに貼り付けた後、UserProduct という型名の上にマウスをホバーしてみてください。TypeScriptが自動で推論した型がポップアップで表示されます。

// ① このコードをコピーしてPlaygroundに貼り付ける
interface ApiResponse<T> {
  data: T;
  meta: { page: number; total: number };
  error?: string;
}
 
type GetReturn<T> = T extends (...args: any[]) => infer R ? R : never;
type ExtractData<T> = T extends ApiResponse<infer U> ? U : never;
 
async function fetchUser() {
  return {
    data: { id: 1, name: "Alex", email: "alex@example.com" },
    meta: { page: 1, total: 1 }
  };
}
 
async function fetchProduct() {
  return {
    data: { id: 1, title: "Laptop", price: 1000 },
    meta: { page: 1, total: 5 }
  };
}
 
// ② この2行の `User` `Product` にマウスをホバーしてみよう
type User = ExtractData<GetReturn<typeof fetchUser>>;
type Product = ExtractData<GetReturn<typeof fetchProduct>>;

ホバーすると何が見える?

User にホバーすると、TypeScriptが自動で以下を推論していることが確認できます:

type User = {
  id: number;
  name: string;
  email: string;
}

自分でinterfaceを書いていないのに、TypeScriptがfetch関数の戻り値から data の型を自動抽出しているのが視覚的に分かります。これがこのテクニックの核心です。

5. 課題 – シニア向けのチャレンジ問題

課題1: 複数のラッパー型に対応する

APIに2種類のレスポンスがあるとします:

type ApiResponse<T> = { data: T; meta: any };
type PaginatedResponse<T> = { items: T[]; total: number; page: number };

以下の条件を満たす ExtractPayload<T> を実装してください:

  • ApiResponse<U>U を返す
  • PaginatedResponse<U>U[] を返す
  • どちらでもない → T をそのまま返す

💡 ヒント: Conditional Types + infer を使います。

✅ 解答を見る(クリック)
type ExtractPayload<T> =
  T extends ApiResponse<infer U> ? U :
  T extends PaginatedResponse<infer U> ? U[] :
  T;

課題2: DeepExtract – ネストされたデータを掘り出す

以下のような深いネスト構造があるとします:

type DeepWrapper<T> = {
  status: string;
  result: {
    data: T;
    meta: any;
  };
};

DeepExtractData<T> を実装して、内部の T を直接抽出してください。

✅ 解答を見る(クリック)
type DeepExtractData<T> =
  T extends DeepWrapper<infer U> ? U : T;

課題3(ボーナス): 型安全なセレクター関数

以下の関数を実装してください:

function selectData<T>(response: T): ExtractData<T> {
  // 実装せよ
}

要件:

  • ジェネリック型を手動で指定しなくても、正しい戻り値の型が推論される
  • 使用例:const userData = selectData(await fetchUser());
✅ 解答を見る(クリック)
function selectData<T>(response: T): ExtractData<T> {
  return (response as any).data;
}

// TypeScriptが自動的に型を推論
const userData = selectData(await fetchUser());
// userDataの型: { id: number; name: string; email: string }

6. まとめ

今日学んだこと

技術 説明
Conditional Types T extends U ? X : Y – 型レベルでの条件分岐
infer キーワード パターンマッチング中に一時的に型をキャプチャする
GetReturn<T> 関数の戻り値の型を抽出(Promise対応)
ExtractData<T> ApiResponse から data の型を抽出
組み合わせ技 複数のUtilityを連鎖させて強力な型推論を実現

次回予告

Part 2: Mapped Types

  • 既存の型を変換・加工する強力なパターン
  • Readonly<T>Partial<T> の内部実装を自作する
  • フォームバリデーション、権限管理など実務での活用例

Have a nice day!

9
5
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
9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?