generics-as-functions について
ジェネリック型を型に対する関数として捉え、制約、命名、ドキュメント作成を案内する TypeScript スキルです。再利用可能なジェネリック型や関数の設計に役立ちます。
主な機能
- 01ジェネリックを型変換として捉える
- 02extends による型パラメーターの制約
- 03意味が伝わる型パラメーター名
- 04@template によるパラメーターの文書化
- 05問題のあるジェネリックパターンの特定
活用シーン
- 01再利用可能な型ユーティリティの定義
- 02ジェネリック関数やクラスの設計
- 03不正な型引数の防止
- 04公開ジェネリックのドキュメント改善
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill generics-as-functionsインストール後、Claude Code で/generics-as-functionsを入力して読み込まれたか確認します。
詳細
ジェネリクスは型間の関数として考える
概要
ジェネリック型は、値の領域における関数に相当する型レベルのものです。関数がパラメーターを受け取り、値を返すように、ジェネリック型は型パラメーターを受け取り、具体的な型を生成します。この考え方を使うと、関数を書くときと同じ原則を適用して、より優れたジェネリック型を書けるようになります。入力を制約し、適切な名前を選び、動作をドキュメント化できます。
ジェネリクスを型間の関数として捉えることで、制約を使うタイミング、型パラメーターの命名方法、そして一部のジェネリックパターンが機能し、別のパターンが機能しない理由が明確になります。この視点は、TypeScript で効果的な型レベルプログラミングを行ううえで欠かせません。
このスキルを使用する場面
- 他の型を変換するジェネリック型を定義する
- 型パラメーターを使用するジェネリック関数を書く
- ジェネリクスに渡せる型を制約する
- TSDoc を使用してジェネリック型をドキュメント化する
- 再利用可能な型ユーティリティを作成する
絶対原則
ジェネリック型は型間の関数として考えます。型注釈のように extends で入力を制約し、説明的な名前を選び、@template TSDoc でドキュメント化します。
見分け方
次のパターンに注意してください。
// RED FLAGS - Poor generic design
type MyPick<T, K> = { [P in K]: T[P] }; // No constraints, errors in implementation
type BadGeneric<X, Y, Z> = ...; // Single-letter names without context
function parse<T>(input: string): T; // Return-only generic, no better than any
関数としてのジェネリック型
ジェネリック型は型パラメーターを受け取り、具体的な型を生成します。
// Generic type "function"
type MyPartial<T> = { [K in keyof T]?: T[K] };
// "Calling" the function with Person
type PartPerson = MyPartial<Person>;
// Equivalent to: { name?: string; age?: number; }
関数と同様に、ジェネリック型は複数のパラメーターを持つことができます。
// Two type parameters
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
// Usage
type NameOnly = MyPick<Person, 'name'>;
// Equivalent to: { name: string }
型パラメーターの制約
関数パラメーターに型注釈を付けるように、extends を使用して型パラメーターを制約します。
// GOOD: Constrained type parameters
type MyPick<T extends object, K extends keyof T> = {
[P in K]: T[P]
};
// Without constraints - allows invalid instantiations
type BadPick<T, K> = { [P in K]: T[P] }; // Errors in implementation
// Invalid uses caught by constraints:
type Bad1 = MyPick<Person, 'firstName'>; // Error: 'firstName' not in Person
type Bad2 = MyPick<'age', Person>; // Error: string doesn't satisfy object
型パラメーターの命名
特に複雑なジェネリクスでは、説明的な名前を選びます。
// Short names OK for simple, local generics
type Partial<T> = { [K in keyof T]?: T[K] };
// Longer names for complex or exported generics
type MapValues<
ObjectType extends object,
ValueTransformer extends (value: any) => any
> = {
[Key in keyof ObjectType]: ValueTransformer<ObjectType[Key]>
};
ジェネリクスのドキュメント化
@template TSDoc タグを使用して型パラメーターをドキュメント化します。
/**
* Construct a new object type using a subset of properties from another.
* @template T - The original object type
* @template K - The keys to pick, typically a union of string literal types
*/
type MyPick<T extends object, K extends keyof T> = {
[P in K]: T[P]
};
ジェネリック関数
ジェネリック関数は関連するジェネリック型を定義し、型推論を可能にします。
function pick<T extends object, K extends keyof T>(
obj: T,
...keys: K[]
): Pick<T, K> {
const result: Partial<Pick<T, K>> = {};
for (const k of keys) {
result[k] = obj[k];
}
return result as Pick<T, K>;
}
// TypeScript infers types from arguments
const person = { name: 'Alice', age: 30 };
const nameOnly = pick(person, 'name');
// Type: Pick<{ name: string; age: number }, 'name'>
ジェネリッククラス
ジェネリッククラスは、メソッドに渡す必要のない型を保持します。
class Box<T> {
value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
// Type inferred from constructor
const dateBox = new Box(new Date());
// Type: Box<Date>
圧力に対する防御プロトコル
制約のないジェネリクスを使うよう求められた場合:
- 制約を追加する:
extendsを使用して、有効な型引数を制限します - デフォルトを検討する:型パラメーターに妥当なデフォルトを指定します
- 要件をドキュメント化する:TSDoc を使用して制約を説明します
- エッジケースをテストする:ユニオン型やエッジケースでもジェネリクスが機能することを確認します
注意すべき兆候
| アンチパターン | 問題点 |
|---|---|
| 制約のない型パラメーター | 無効なインスタンス化や実装エラーを許してしまう |
| 複雑なジェネリクスでの1文字の名前 | 可読性が低下する |
| 戻り値にしか使わないジェネリクス | 型アサーションと同等で、型安全性がない |
| 公開ジェネリクスに TSDoc がない | 開発者エクスペリエンスが損なわれる |
よくある正当化
「制約によって柔軟性が損なわれる」
実際には:制約を使えば、わかりにくい型エラーや誤った型を生じさせる代わりに、型レベルでエラーを検出できます。また、有効な使用方法を記録できます。
「T、K、V は標準的な名前だ」
実際には:単純なケースでは慣例的に使われますが、複雑なジェネリクスでは説明的な名前のほうが読みやすくなります。名前の長さはスコープに合わせましょう。
「ユーザーは型を理解できる」
実際には:ドキュメントがあれば、ユーザーは実装を読まずにジェネリクスを理解できます。@template タグは IDE のツールチップに表示されます。
クイックリファレンス
| 概念 | 値レベル | 型レベル |
|---|---|---|
| 定義 | function | type |
| パラメーター | (x: T) | <T extends Constraint> |
| 戻り値 | : ReturnType | = ResultType |
| ドキュメント | @param | @template |
| 制約 | 型注釈 | extends キーワード |
要点
ジェネリック型は型間の関数です。関数を書くときと同じ原則を適用しましょう。入力を制約し、意味のある名前を選び、十分にドキュメント化します。この考え方により、複雑な型レベルコードが理解しやすく、保守しやすくなります。
参考資料
- Dan Vanderkam 著『Effective TypeScript』第2版
- 項目50:ジェネリクスは型間の関数として考える