📝 注記
私は日本語に堪能ではありません。この記事はAIのサポートを受けて執筆・翻訳されています。ご了承ください。よろしくお願いいたします。
📖 目次
- 問題の提示 – どんな時にこのテクニックが必要か
- 悪い例 – まずはダメなコードを見せる
- 良い例 – TypeScriptの高度機能で解決する
- Playgroundリンク – その場で試せる
- 課題 – シニア向けのチャレンジ問題
- まとめ
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に貼り付けた後、User や Product という型名の上にマウスをホバーしてみてください。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!
