currying-inference について
TypeScript のコンパイラーがジェネリック型を推論しやすい API 設計を学ぶスキルです。引数、カリー化、クラス、ファクトリー関数で推論の機会を増やす方法を説明します。
主な機能
- 01型推論サイトの作成
- 02カリー化関数の活用
- 03クラスとファクトリー関数の活用
- 04ジェネリック制約の改善
活用シーン
- 01型を推論できるビルダー API の設計
- 02unknown と推論されるジェネリック型の修正
- 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「クラスとカリー化を使って新しい推論サイトを作成する」に基づいています。