mcpField
T
typescript-advanced-patterns

typescript-advanced-patterns

github.com/curiositech開発263140 回インストール2 日前に更新
リポジトリ

typescript-advanced-patterns について

コンパイル時の制約によって実行時エラーを減らす、高度な型システムパターンを扱う TypeScript 開発者向けスキルです。ブランド型、ユニオン、スキーマ推論などを解説し、単純な型を選ぶべき場面にも触れます。

主な機能

  1. 01ブランド型と名目的型付け
  2. 02判別可能ユニオンと網羅性チェック
  3. 03テンプレートリテラル型と条件型
  4. 04Zod スキーマ推論
  5. 05型安全なイベントエミッターパターン

活用シーン

  1. 01異なる識別子の混同防止
  2. 02API 結果や状態バリアントのモデル化
  3. 03検証済み外部データからの型導出
  4. 04型付きイベントを持つエミッターの構築

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/curiositech/some_claude_skills --skill typescript-advanced-patterns

インストール後、Claude Code で/typescript-advanced-patternsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

.claude-plugin/
plugin.json
references/
branded-types.md
type-safe-patterns.md

詳細

TypeScript の高度なパターン

コンパイル時に制約をエンコードすることで、実行時のバグをなくす高度な型システムのパターンです。

使用するタイミング

次のような場合に有効化: 「ブランド型」「名目的型付け」「判別共用体」「テンプレートリテラル型」「条件型」「infer キーワード」「satisfies 演算子」「const アサーション」「Zod の型推論」「網羅性のある switch」「型安全なイベントエミッター」「マップ型」「ユーティリティ型」「ジェネリック制約」「型の絞り込み」「as const」

対象外: TypeScript の基本構文 | React コンポーネントのプロパティ型 | JavaScript の一般的なパターン

問題に適した高度なパターンを選ぶための判断ツリー

flowchart TD
    P[What is your problem?] --> P1[I'm mixing up\nvalues of the same type\ne.g. UserId vs OrderId]
    P[What is your problem?] --> P2[I have a value that\ncan be one of N shapes]
    P[What is your problem?] --> P3[I need type-safe\nevent emitting]
    P[What is your problem?] --> P4[I'm parsing external\ndata and want types]
    P[What is your problem?] --> P5[I want types that\ndepend on other types]
    P[What is your problem?] --> P6[My switch/if is\nnot exhaustive]

    P1 --> S1[Branded Types\nfor nominal typing]
    P2 --> S2[Discriminated Unions\nwith type narrowing]
    P3 --> S3[Type-Safe Event Emitter\nwith mapped types]
    P4 --> S4[Zod schema + z.infer\nfor runtime + compile time]
    P5 --> S5[Conditional Types\nwith infer keyword]
    P6 --> S6[Exhaustive checking\nwith never type]

主なパターン

1. ブランド型(名目的型付け)

TypeScript の構造的型システムでは、UserId と string は互いに代入可能です。ブランド型はファントムブランドを追加し、これらを互換性のない型にします。

// Without branding: these compile without error
function chargeUser(userId: string, amount: number) { /* ... */ }
const orderId = getOrderId();
chargeUser(orderId, 100); // Wrong! orderId passed as userId — TypeScript allows it

// With branding: compile-time protection
type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
type EmailAddress = Brand<string, 'EmailAddress'>;
type Cents = Brand<number, 'Cents'>;  // Never pass raw numbers as money

// Constructor functions (only way to create branded values)
const UserId = (id: string): UserId => id as UserId;
const OrderId = (id: string): OrderId => id as OrderId;
const Cents = (n: number): Cents => {
  if (!Number.isInteger(n) || n < 0) throw new Error(`Invalid cents: ${n}`);
  return n as Cents;
};

// Usage: type error if you mix them up
function chargeUser(userId: UserId, amount: Cents) { /* ... */ }

const uid = UserId('user_123');
const oid = OrderId('order_456');

chargeUser(uid, Cents(1000));  // OK
chargeUser(oid, Cents(1000));  // Error: Argument of type 'OrderId' is not assignable to 'UserId'
chargeUser(uid, 1000);         // Error: Argument of type 'number' is not assignable to 'Cents'

Zod との統合やデータベースモデルのパターンについては、references/branded-types.md を参照してください。

2. 型の絞り込みを伴う判別共用体

kind(または type、tag)フィールドが判別子です。これをチェックすると、TypeScript は共用体を絞り込みます。

type ApiResult<T> =
  | { status: 'success'; data: T }
  | { status: 'error'; code: number; message: string }
  | { status: 'loading' };

function handleResult<T>(result: ApiResult<T>): T | null {
  switch (result.status) {
    case 'success': return result.data;        // TypeScript knows data exists here
    case 'error':   console.error(result.code, result.message); return null;
    case 'loading': return null;
  }
}

網羅性チェックを行う場合 — 新しいバリアントが追加され、switch が更新されていないと、コンパイルエラーになります。

function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${JSON.stringify(x)}`);
}

function handleResult<T>(result: ApiResult<T>): T | null {
  switch (result.status) {
    case 'success': return result.data;
    case 'error':   return null;
    // If 'loading' is not handled, TypeScript errors:
    // Argument of type '{ status: "loading" }' is not assignable to 'never'
    default: return assertNever(result);
  }
}
3. テンプレートリテラル型

型レベルで文字列リテラルを組み合わせ、文字列ベースの API に対して厳密な型を作成します。

// Route path types — prevents typos in route definitions
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type ApiVersion = 'v1' | 'v2';
type Resource = 'users' | 'orders' | 'products';

type ApiEndpoint = `/${ApiVersion}/${Resource}`;
// Type: "/v1/users" | "/v1/orders" | "/v1/products" | "/v2/users" | ...

// CSS-in-JS property types
type CSSProperty = 'margin' | 'padding' | 'border';
type Side = 'top' | 'right' | 'bottom' | 'left';
type CSSPropertyWithSide = `${CSSProperty}-${Side}`;
// Type: "margin-top" | "margin-right" | ... | "border-left"

// Event name types
type DOMEventMap = {
  click: MouseEvent;
  keydown: KeyboardEvent;
  input: InputEvent;
};
type EventName = `on${Capitalize<keyof DOMEventMap>}`;
// Type: "onClick" | "onKeydown" | "onInput"
4. infer を使った条件型

条件型の評価中に、infer は別の型の内部から型を抽出します。

// Extract the return type of an async function
type Awaited<T> = T extends Promise<infer U> ? U : T;

type UserFetch = () => Promise<{ id: string; name: string }>;
type User = Awaited<ReturnType<UserFetch>>;
// Type: { id: string; name: string }

// Extract the element type of an array
type ElementOf<T> = T extends (infer U)[] ? U : never;
type Names = ElementOf<string[]>;  // string
type Events = ElementOf<Array<{ id: number; type: string }>>;  // { id: number; type: string }

// Extract handler parameters from an event map
type HandlerParams<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

// Deeply unwrap Promises
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type Result = DeepAwaited<Promise<Promise<string>>>;  // string
5. satisfies 演算子(TypeScript 4.9+)

satisfies は値の型を拡大せずに、型に対して値を検証します。型チェックと、最も具体的な推論型の両方が得られます。

type Color = string | [number, number, number];

// Problem with 'as': loses specific type info
const colors = {
  red: [255, 0, 0] as Color,    // type is Color, not [255, 0, 0]
  blue: [0, 0, 255] as Color,
} as Record<string, Color>;

// Problem with annotation: same widening
const colors2: Record<string, Color> = {
  red: [255, 0, 0],   // type is Color
};

// satisfies: validates AND preserves specific type
const colors3 = {
  red: [255, 0, 0],
  blue: [0, 0, 255],
} satisfies Record<string, Color>;

colors3.red.map(x => x * 2);   // OK: TypeScript knows it's [number, number, number]
colors3.blue[0];                // OK: indexed access works

// Great for config objects
const config = {
  port: 3000,
  host: 'localhost',
  debug: false,
} satisfies {
  port: number;
  host: string;
  debug: boolean;
  timeout?: number;  // optional fields allowed to be absent
};

config.port.toFixed(0);   // OK: port is still `number`, not widened to `number | string`
6. Zod スキーマの型推論

Zod は実行時に検証し、TypeScript の型も自動で提供します。型とバリデーターを別々に記述しないでください。

import { z } from 'zod';

// Define schema once
const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'moderator']),
  createdAt: z.coerce.date(),
  metadata: z.record(z.string(), z.unknown()).optional(),
});

// Extract type — no duplicate type definition
type User = z.infer<typeof UserSchema>;
// Type: { id: string; email: string; role: "admin" | "user" | "moderator"; createdAt: Date; metadata?: Record<string, unknown> }

// Parse with validation
function parseUser(raw: unknown): User {
  return UserSchema.parse(raw);  // throws ZodError with structured errors on failure
}

// Safe parse (returns Result type)
const result = UserSchema.safeParse(rawData);
if (result.success) {
  result.data.role;  // narrowed to User
} else {
  result.error.issues;  // structured validation errors
}

// Derive related schemas
const CreateUserInput = UserSchema.omit({ id: true, createdAt: true });
type CreateUserInput = z.infer<typeof CreateUserInput>;

const UpdateUserInput = UserSchema.partial().required({ id: true });

型安全なイベントエミッター、ビルダーパターン、網羅性チェック用ユーティリティについては、references/type-safe-patterns.md を参照してください。

リファレンスファイル

ファイル内容
references/branded-types.mdID/通貨/メールアドレス向けのブランド付きプリミティブ、Zod との統合、データベースのパターン
references/type-safe-patterns.md網羅性のある switch、ビルダーパターン、型安全なイベントエミッター、マップ型ユーティリティ

アンチパターン(見識の指標)

アンチパターン 1: 適切なジェネリックの代わりに any を逃げ道として使う

初心者の考え方: 「型を付けるには複雑すぎるので、とりあえず any を使って、あとで対応しよう。」

なぜ誤りなのか: any はその値に対するすべての型チェックを無効にし、さらにその値が関わるあらゆる箇所に広がります。any は伝染します。値がいったん any になると、それを受け取る関数は戻り値の型も any と推論され、時間とともに拡大する型の穴ができます。「あとで対応する」ことは決してありません。

検出方法: コードベースにある as any は、ほぼ常に TypeScript の制限ではなく、型設計の問題を示しています。

解決策 — 制約付きジェネリックを使う:

// Bad: uses any to avoid figuring out the right type
function processItems(items: any[]): any[] {
  return items.filter(item => item.active);
}

// Good: generic with constraint
function processItems<T extends { active: boolean }>(items: T[]): T[] {
  return items.filter(item => item.active);
}

// Better: if you just need "has a property", use unknown and narrow
function getProperty(obj: unknown, key: string): unknown {
  if (typeof obj === 'object' && obj !== null && key in obj) {
    return (obj as Record<string, unknown>)[key];
  }
  return undefined;
}

解決策 — 本当に不明なデータには any ではなく unknown を使う:

// Bad: any propagates
function parseData(raw: any) {
  return raw.user.id;  // No error, will crash if shape is wrong
}

// Good: unknown forces you to narrow
function parseData(raw: unknown) {
  if (
    typeof raw === 'object' &&
    raw !== null &&
    'user' in raw &&
    typeof (raw as any).user === 'object'
  ) {
    // Now you can access safely
  }
  // Better: use Zod
  return UserSchema.parse(raw);
}

見識の指標: any は「わからないし、TypeScript にチェックさせたくない」という意味です。unknown は「わからないが、使う前にチェックする」という意味です。API 境界では unknown を優先し、any は決して使わないでください。


アンチパターン 2:よりシンプルな型で十分なのに、型を過剰に設計する

初心者の考え方(TypeScript に詳しい場合):「条件付き型とテンプレートリテラル型を使えば、ビジネスルール全体を型システムで表現できます!」

なぜ間違いなのか:型の複雑さには、実際のコストが伴います。infer の階層が複数ある条件付き型のエラーメッセージは、読み解くのに 3 分かかることがあります。若手エンジニアには保守できません。IDE の自動補完は非常に遅くなります。型の計算に 200ms かかると、ファイル全体の編集体験が損なわれます。

判断のための問い:型の複雑さによって、実際に起こり得る種類のバグを防げますか? そうであれば、採用する価値があります。単に実現できることを証明したいだけなら、シンプルにしましょう。

適切な例と過剰設計の例:

// APPROPRIATE: Branded types prevent real mixing bugs
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;

// APPROPRIATE: Discriminated union encodes real state machine
type AuthState =
  | { status: 'logged_out' }
  | { status: 'logging_in' }
  | { status: 'logged_in'; user: User; token: string };

// OVER-ENGINEERED: Encoding HTTP method semantics in types
// Does this actually prevent bugs? Probably not.
type HttpMethodHasBody<M extends HttpMethod> =
  M extends 'POST' | 'PUT' | 'PATCH' ? true : false;

type RequestWithBody<M extends HttpMethod> =
  HttpMethodHasBody<M> extends true
    ? { method: M; body: unknown }
    : { method: M; body?: never };

// OVER-ENGINEERED: Recursive type for deeply nested access
// Causes slow compilation and unreadable errors
type DeepGet<T, Path extends string> =
  Path extends `${infer Head}.${infer Tail}`
    ? Head extends keyof T
      ? DeepGet<T[Head], Tail>
      : never
    : Path extends keyof T
      ? T[Path]
      : never;
// Use lodash.get + unknown return type instead

判断のヒューリスティック:型を一文で説明できない場合は、コメントが必要です。二文を超える場合は、よりシンプルな方法(実行時検証やヘルパー関数)のほうが保守しやすくないか、再検討してください。

見分けるポイント:TypeScript の熟練エンジニアは、高度な型を使わないほうがよい場面を理解しています。目指すのは、バグを減らし、開発者体験を向上させることであり、型システムの習熟度を示すことではありません。同じようにバグを防げるのであれば、複雑な条件付き型よりシンプルなユニオン型を選びましょう。

品質チェックリスト

[ ] No bare `any` types — unknown or generics used instead
[ ] Primitive values that must not be mixed are branded (IDs, money, emails)
[ ] Sum types use discriminated unions, not boolean flags
[ ] External data parsed through Zod schemas (never typed as a known type without validation)
[ ] Switch statements over union types use exhaustive checking
[ ] Conditional types include a comment explaining what they compute
[ ] No type aliases that just rename primitives without branding
[ ] Generic constraints are as specific as needed, no more
[ ] satisfies used for config objects instead of widening assertions
[ ] Type-level tests (expect-type) for complex utility types

出力成果物

  1. ドメイン型モジュール — すべてのプリミティブなドメイン値(ID、金額、メールアドレス)のブランド型
  2. Zod スキーマ — z.infer 型をエクスポートするスキーマ定義
  3. 判別可能なユニオンの定義 — ステートマシン、API の結果、ドメインイベント
  4. 型安全なイベントエミッター — 型付きイベントマップを備えたジェネリックな EventEmitter
  5. ユーティリティ型 — プロジェクトで再利用できる条件付き型ユーティリティ

コメント