avoid-anecdotal-types について
API、データベース、サードパーティーデータの型を定義する開発者向けスキルです。単一のサンプルから推測せず、仕様や複数の例、実行時検証を重視します。
主な機能
- 01サンプルより仕様を優先
- 02複数のデータ例を確認
- 03生成済み型定義を利用
- 04実行時のデータ検証
活用シーン
- 01外部 API の型定義
- 02データベースデータのモデル化
- 03サードパーティーサービスの応答処理
- 04予期しない型エラーの調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill avoid-anecdotal-typesインストール後、Claude Code で/avoid-anecdotal-typesを入力して読み込まれたか確認します。
詳細
逸話的なデータに基づく型を避ける
概要
1つの例から型を推測しないでください。
1つの API レスポンスやサンプルに基づいて型を作成すると、エッジケースを見落とします。データの全体像を把握するために、信頼できる型定義を入手するか、複数の例を調べてください。
このスキルを使う場面
- 外部 API の型を作成するとき
- データベースのデータに型を付けるとき
- サードパーティサービスを利用するとき
- 「ありえない」型エラーをデバッグするとき
鉄則
Types should be based on specifications, not samples.
One example doesn't show all possibilities.
覚えておきましょう:
- API にはオプションのフィールド、バリエーション、エッジケースがあります
- サンプルデータが示すのは一般的なケースであり、すべてのケースではありません
- 「自分のデータでは動く」≠「型が正しい」
- 例だけでなく、仕様を入手してください
検出:例に基づく型
// Created from looking at one API response
interface GeocodingResult {
lat: number;
lng: number;
address: string;
city: string;
country: string;
}
// But the real API might return:
// - Missing city for some locations
// - Multiple results (array)
// - Error responses
// - Different formats for different countries
信頼できる情報源
1. 公式ドキュメント/仕様
// From API documentation:
interface GeocodingResult {
lat: number;
lng: number;
formatted_address: string;
address_components: AddressComponent[]; // Not flat city/country!
partial_match?: boolean; // Optional!
}
2. TypeScript/JSON Schema の定義
// Many APIs provide TypeScript types
import { GeocodingResult } from '@google/maps-types';
// Or JSON Schema
import Ajv from 'ajv';
const validate = ajv.compile(geocodingSchema);
3. OpenAPI/GraphQL から生成された型
// Auto-generated from API specification
import { GeocodingResponse } from './generated/api-types';
実例:GitHub API
// From looking at one user:
interface GitHubUser {
id: number;
login: string;
name: string;
email: string;
company: string;
bio: string;
}
// Reality from GitHub's API docs:
interface GitHubUser {
id: number;
login: string;
name: string | null; // Can be null!
email: string | null; // Can be null!
company: string | null; // Can be null!
bio: string | null; // Can be null!
// ... many more fields, some conditional
}
あるユーザーの例では、すべてのフィールドに値が入っていました。しかし、そうでないユーザーも多くいます。
JSON.parse の問題
// Dangerous: trusting parsed JSON
const response = await fetch('/api/user');
const user: User = await response.json(); // No runtime validation!
// The server might return:
// - Missing fields
// - Extra fields
// - Different types than expected
// - Error responses
実行時検証
データが型と一致することを確認するため、実行時検証を使ってください。
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string().nullable(),
email: z.string().email().nullable(),
});
type User = z.infer<typeof UserSchema>;
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/user/${id}`);
const data = await response.json();
return UserSchema.parse(data); // Throws if invalid
}
これで、本番環境でバグが起きる前に、実行時に不一致を検出できます。
複数の例
仕様を入手できない場合は、複数の例を調べてください。
// Look at edge cases:
// - New user (minimal data)
// - Old user (all fields filled)
// - Deleted user (special state)
// - Admin user (extra fields)
// - Different locales
// Build type that handles ALL cases:
interface User {
id: number;
login: string;
name: string | null;
role?: 'user' | 'admin';
deletedAt?: Date;
}
よくある逸話的なパターン
「自分のデータにはこのフィールドがある」
// Your test data:
const user = { name: "Alice", age: 30 };
// But age is optional for some users!
interface User {
name: string;
age?: number; // Should be optional
}
「API は常に……を返す」
// Your experience:
const response = { data: [...], total: 100 };
// But empty results might differ:
const emptyResponse = { data: [], total: 0 }; // OK
const notFound = { error: "Not found" }; // Oops, no data field!
「このフィールドは常に数値である」
// Your examples:
{ id: 123 }
{ id: 456 }
// But some systems return:
{ id: "abc-123" } // String in some cases!
圧力への対処プロトコル
1. 「例では動く」
圧力:「テストしたけれど、問題なく動きます」
回答: テストデータでは動きます。エッジケースはどうでしょうか?
対応: 仕様を入手するか、さまざまなデータでテストしてください。
2. 「ドキュメントがない」
圧力:「API 仕様がありません」
回答: 複数のレスポンスを調べてください。エラーケースを確認してください。API の管理者に問い合わせてください。
対応: オプションのフィールドと検証を含む、防御的な型を作成してください。
危険信号 — 作業を止めて再検討する
- 1つの JSON レスポンスをコピーして型を作成している
- API に常に含まれるとは限らないプロパティがすべて必須になっている
- エラー状態や空の状態への対応が欠けている
- 「正常系」のデータにしか一致しない型
よくある正当化(すべて不適切)
| 言い訳 | 実際のところ |
|---|---|
| 「テストでは動く」 | テストで確認できるのは1つの経路であり、すべてではありません |
| 「API は自分たちで管理している」 | API は変更され、エッジケースもあります |
| 「壊れたら修正する」 | エッジケースに最初に遭遇するのはユーザーです |
クイックリファレンス
// DON'T: Type from one example
interface User {
id: number;
name: string; // What if null?
email: string; // What if missing?
}
// DO: Type from specification
interface User {
id: number;
name: string | null;
email?: string;
}
// DO: Runtime validation
const UserSchema = z.object({
id: z.number(),
name: z.string().nullable(),
email: z.string().optional(),
});
// DO: Get official types
import { User } from '@api/types';
要点
サンプルに基づく型では、エッジケースを見落とします。
1つの API レスポンスや1つのデータベースレコードからは、あらゆる可能性はわかりません。公式ドキュメント、生成された型、または実行時検証を使ってください。例から型を作成する場合は、エッジケースやエラーを含む複数のケースを調べてください。
参考文献
Dan Vanderkam 著『Effective TypeScript』項目 42「逸話的なデータに基づく型を避ける」に基づいています。