mcpField
C
currying-inference

currying-inference

github.com/marius-townhouse開発644 回インストール38 週間前に更新
リポジトリ

currying-inference について

TypeScript のコンパイラーがジェネリック型を推論しやすい API 設計を学ぶスキルです。引数、カリー化、クラス、ファクトリー関数で推論の機会を増やす方法を説明します。

主な機能

  1. 01型推論サイトの作成
  2. 02カリー化関数の活用
  3. 03クラスとファクトリー関数の活用
  4. 04ジェネリック制約の改善

活用シーン

  1. 01型を推論できるビルダー API の設計
  2. 02unknown と推論されるジェネリック型の修正
  3. 03明示的な型引数や型アサーションの削減

インストール

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

$npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill currying-inference

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

詳細

クラスとカリー化を使って新しい推論サイトを作成する

概要

TypeScript がジェネリック型を推論できない場合は、新しい推論機会を作成します。

TypeScript は、特定の「推論サイト」でジェネリック型パラメーターを推論します。あるサイトで十分な情報が得られない場合は、クラス、カリー化、またはヘルパー関数を使って推論サイトを追加できます。

このスキルを使う場面

  • ジェネリック型パラメーターが推論されない
  • ビルダーパターンで型推論が必要
  • メソッドチェーンで型情報が失われる
  • 型推論を促す API を作成する

絶対ルール

Create inference sites where TypeScript needs type information.
Classes and curried functions provide natural inference points.

覚えておきましょう:

  • 推論は関数呼び出しとクラスのインスタンス化で行われます
  • 推論サイトが多いほど、型推論の精度が向上します
  • カリー化により、複数の呼び出しに推論を分割できます
  • クラスではコンストラクターで推論できます

検出:推論サイトがない場合

declare function fetchData<T>(url: string, options: RequestOptions): Promise<T>;

// TypeScript can't infer T
const data = await fetchData('/api/users', { method: 'GET' });
//    ^? unknown

// Must specify explicitly
const data = await fetchData<User[]>('/api/users', { method: 'GET' });

型パラメーター T には推論サイトがありません。

解決策 1:パラメーターで推論サイトを追加する

declare function fetchData<T>(
  url: string,
  parser: (raw: unknown) => T
): Promise<T>;

const data = await fetchData('/api/users', (raw) => raw as User[]);
// ^? User[]

parser 関数によって、T の推論サイトが提供されます。

解決策 2:カリー化関数

// Single function: T not inferred
function makeRequest<T>(url: string): Promise<T>;

// Curried: inference at each call
function makeRequest<T>() {
  return (url: string): Promise<T> => {
    return fetch(url).then(r => r.json());
  };
}

// Usage creates inference site
const getUsers = makeRequest<User[]>();
const users = await getUsers('/api/users');

解決策 3:クラスを使ったビルダーパターン

class RequestBuilder<T = unknown> {
  private url: string = '';
  
  setUrl(url: string): this {
    this.url = url;
    return this;
  }
  
  // New method creates new inference site
  withParser<U>(parser: (data: unknown) => U): RequestBuilder<U> {
    return this as unknown as RequestBuilder<U>;
  }
  
  async execute(): Promise<T> {
    const response = await fetch(this.url);
    return response.json();
  }
}

// Type inferred from parser
const users = await new RequestBuilder()
  .setUrl('/api/users')
  .withParser((data): User[] => data as User[])
  .execute();
// ^? User[]

実践例:イベントエミッター

// Without inference sites
class EventEmitter {
  on<T>(event: string, handler: (data: T) => void): void;
  emit<T>(event: string, data: T): void;
}

// TypeScript can't connect the T's
emitter.on('user', (data) => {
  //                ^? unknown
});

// With type map
interface EventMap {
  user: User;
  message: Message;
}

class TypedEventEmitter<Events extends Record<string, any>> {
  on<K extends keyof Events>(
    event: K, 
    handler: (data: Events[K]) => void
  ): void;
  
  emit<K extends keyof Events>(
    event: K, 
    data: Events[K]
  ): void;
}

const emitter = new TypedEventEmitter<EventMap>();
emitter.on('user', (data) => {
  //                ^? User
});

ファクトリー関数

// Factory provides inference site
function createStore<T>(initial: T) {
  let state = initial;
  return {
    get: () => state,
    set: (newState: T) => { state = newState; }
  };
}

const userStore = createStore({ name: 'Alice', age: 30 });
// T inferred from initial value

userStore.set({ name: 'Bob', age: 25 }); // OK
userStore.set({ name: 'Charlie' }); // Error: missing age

型の変化を伴うメソッドチェーン

class QueryBuilder<T = unknown, Selected = T> {
  select<K extends keyof T>(...keys: K[]): QueryBuilder<T, Pick<T, K>> {
    return this as any;
  }
  
  where(predicate: (item: T) => boolean): QueryBuilder<T, Selected> {
    return this as any;
  }
  
  execute(): Selected[] {
    // ...
  }
}

interface User { id: number; name: string; email: string; age: number; }

const results = new QueryBuilder<User>()
  .select('name', 'email')
  .where(u => u.age > 18)
  .execute();
// ^? { name: string; email: string; }[]

推論を改善するジェネリック制約

// Without constraint
function pluck<T, K>(items: T[], key: K): T[K][];
// K not constrained, inference poor

// With constraint
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map(item => item[key]);
}

const names = pluck(users, 'name');
// ^? string[]

制約 K extends keyof T により、TypeScript は T から K を推論しやすくなります。

戻り値の型にしか使われない型パラメーターを避ける

// Bad: T only in return position (no inference)
function parseJson<T>(): T {
  return JSON.parse(data);
}

// Good: T has inference site
function parseJson<T>(parser: (raw: unknown) => T): T {
  return parser(JSON.parse(data));
}

// Good: Factory pattern
function createParser<T>() {
  return {
    parse: (data: string): T => JSON.parse(data)
  };
}
const userParser = createParser<User>();

プレッシャー耐性プロトコル

1. 「型アサーションを使えばよい」

プレッシャー:「as T でキャストします」

**回答:**型アサーションは型チェックを回避します。推論しやすい設計にするほうが適切です。

**対応:**パラメーターやカリー化を通じて推論サイトを追加します。

2. 「型パラメーターを明示すればよい」

プレッシャー:「ユーザーは fn<Type>(...) と書けばよいのです」

**回答:**型推論を使えば、手間が減り、エラーも起きにくくなります。

**対応:**型推論が自動的に機能する API を設計します。

危険信号 — 中断して再検討する

  • 型パラメーターが戻り値の型にしか使われていないジェネリック関数
  • ユーザーが常にジェネリック型パラメーターを明示する必要がある
  • 特定の型が期待される箇所に unknown や any が現れる
  • 正しい型を得るために型アサーションが必要

よくある不適切な正当化

言い訳実態
「ユーザーが型を指定できます」優れた API 設計では型が推論されます
「複雑すぎます」カリー化とファクトリーは簡潔です
「型アサーションで対応できます」型アサーションは安全性を損ないます。推論のほうが適切です

クイックリファレンス

// BAD: No inference site for T
function fetch<T>(url: string): Promise<T>;

// GOOD: Parser provides inference site
function fetch<T>(url: string, parse: (raw: unknown) => T): Promise<T>;

// GOOD: Currying
function fetch<T>() {
  return (url: string): Promise<T> => ...;
}

// GOOD: Factory
function createFetcher<T>(parse: (raw: unknown) => T) {
  return (url: string): Promise<T> => ...;
}

要点

TypeScript に推論の機会を与える API を設計しましょう。

ジェネリック型を推論できない場合は、型を使うパラメーター、カリー化関数、またはクラスメソッドによって推論サイトを追加します。優れた API 設計では、型パラメーターを明示する必要がありません。

参考資料

Dan Vanderkam 著『Effective TypeScript』の項目 28「クラスとカリー化を使って新しい推論サイトを作成する」に基づいています。

コメント