ゴール:「T だけじゃ足りないときに、自然に型パラメータを増やせるようになる」
ジェネリクスに慣れてくると、
「T だけじゃ足りないな…もう1種類、型を扱いたい」
という場面が必ず出てきます。
複数型パラメータは、そのための仕組みです。
一言でいうと、
「値が複数あるなら、型の“変数”も複数あっていい」
というだけの話です。
ただし、T/U/V をなんとなく増やすのではなく、
「それぞれが何を表しているか」を意識して使うのが大事です。
基本形:T と U の2つを使うジェネリック関数
まずは一番シンプルな例:2つの値をペアにする
複数型パラメータの入門として、
「2つの値をタプルにする関数」を考えてみます。
function pair<T, U>(a: T, b: U): [T, U] {
return [a, b];
}
const p1 = pair(1, "hello"); // [number, string]
const p2 = pair(true, { id: 1 }); // [boolean, { id: number }]
TypeScriptここでやっていることは、とても素直です。
T は第1引数 a の型
U は第2引数 b の型
戻り値は [T, U] というタプル型
という対応になっています。
重要なのは、
「T と U は“別々の型の変数”であり、呼び出しごとにそれぞれ推論される」
ということです。
pair(1, "hello") のときは
T = number, U = string
pair(true, { id: 1 }) のときは
T = boolean, U = { id: number }
というふうに、毎回ちゃんと変わります。
型パラメータを明示して書くと見え方がクリアになる
最初は、あえて型パラメータを明示してみると理解しやすいです。
const p = pair<number, string>(1, "hello"); // [number, string]
TypeScriptこう書くと、
「pair は <number, string> という“型の引数”を受け取り、a: number, b: string, 戻り値 [number, string] の関数として振る舞う」
と読めます。
慣れてきたら、ほとんどの場合は推論に任せて OK です。
複数型パラメータの“関係性”を型で表す
入力と出力の関係を T と U でつなぐ
例えば、「ある型から別の型に変換する」関数を考えます。
function mapValue<TInput, TOutput>(
value: TInput,
fn: (v: TInput) => TOutput
): TOutput {
return fn(value);
}
const len = mapValue("hello", (s) => s.length); // len: number
const upper = mapValue("hello", (s) => s.toUpperCase()); // upper: string
TypeScriptここでは、
TInput は「入力の型」
TOutput は「出力の型」
という役割を持っています。
関数の中身は TInput/TOutput という抽象的な名前で書かれていますが、
呼び出し時には具体的な型(string → number、string → string など)が入ります。
ポイントは、
「複数型パラメータを使うと、“値どうしの関係”を“型どうしの関係”としても表現できる」
というところです。
配列版 map をジェネリクスで書く
もう少し実務寄りの例として、配列の map を考えます。
function mapArray<TInput, TOutput>(
arr: TInput[],
fn: (value: TInput) => TOutput
): TOutput[] {
return arr.map(fn);
}
const lengths = mapArray(["a", "bb", "ccc"], (s) => s.length);
// lengths: number[]
const upper = mapArray(["a", "bb"], (s) => s.toUpperCase());
// upper: string[]
TypeScriptここでも、
TInput は「元の配列の要素型」
TOutput は「変換後の要素型」
という関係になっています。
any で書いてしまうと、
戻り値が any[] になってしまいますが、
ジェネリクス+複数型パラメータを使うことで、
「元の型」と「変換後の型」の関係を、
型レベルで正確に表現できます。
クラスでの複数型パラメータ
キーと値を持つ Map 風クラス
複数型パラメータは、クラスでもよく使います。
class SimpleMap<TKey, TValue> {
private store = new Map<TKey, TValue>();
set(key: TKey, value: TValue) {
this.store.set(key, value);
}
get(key: TKey): TValue | undefined {
return this.store.get(key);
}
}
const userNameById = new SimpleMap<number, string>();
userNameById.set(1, "Taro");
const name = userNameById.get(1); // name: string | undefined
TypeScriptここでは、
TKey は「キーの型」
TValue は「値の型」
という役割です。
SimpleMap<number, string> と書くことで、
「キーは number、値は string のマップ」
という型が表現できます。
重要なのは、
「クラスの中では TKey/TValue としか書いていないのに、
インスタンスごとに具体的な型が違う」
というところです。
new SimpleMap<number, string>()new SimpleMap<string, boolean>()
など、同じクラス定義からいろんなバリエーションを作れます。
制約付きの複数型パラメータ
「この2つの型には関係がある」と表現する
複数型パラメータは、制約(extends)と組み合わせるとさらに表現力が上がります。
例えば、「オブジェクトと、そのキー」を扱う関数。
function getProp<TObj, TKey extends keyof TObj>(
obj: TObj,
key: TKey
): TObj[TKey] {
return obj[key];
}
const user = { id: 1, name: "Taro" };
const id = getProp(user, "id"); // id: number
const name = getProp(user, "name"); // name: string
// getProp(user, "age"); // エラー: "age" は存在しない
TypeScriptここでは、
TObj は「オブジェクト全体の型」
TKey は「TObj のキーのどれか(keyof TObj)」
という関係になっています。
TKey extends keyof TObj という制約で、
「TKey は TObj のキーのどれかに限る」
と宣言しているわけです。
複数型パラメータを使うと、
「TObj と TKey は無関係な2つの型」ではなく、
「TKey は TObj に依存した型」
という“型どうしの関係性”を表現できるようになります。
複数型パラメータでやりがちな失敗とコツ
なんとなく T, U, V を増やしてしまう
よくあるのが、こういう書き方です。
function doSomething<T, U, V>(a: T, b: U, c: V) {
// ...
}
TypeScriptこれだと、T/U/V が何を表しているのか、
コードをじっくり読まないと分かりません。
少しだけ名前に意味を持たせるだけで、かなり読みやすくなります。
function doSomething<TUser, TConfig, TResult>(
user: TUser,
config: TConfig,
initial: TResult
): TResult {
// ...
}
TypeScript完璧な名前をつける必要はありませんが、
「この型パラメータは何の役割なのか」が名前から伝わるようにする
という意識を持つと、複数型パラメータでも迷子になりにくくなります。
本当に複数必要か、一度立ち止まって考える
ときどき、こういうコードも見かけます。
function identity2<T, U>(value: T): T {
return value;
}
TypeScriptU がまったく使われていません。
型パラメータは「増やせば偉い」ものではなく、
「必要な分だけ使う」のが正解です。
「この型パラメータは、本当に別の型として扱う必要があるか?」
「T だけで表現できないか?」
と一度立ち止まって考える癖をつけると、
ジェネリクスがスッキリしたまま保てます。
まとめ:複数型パラメータを自分の言葉で説明すると
最後に、あなた自身の言葉でこう整理してみてください。
複数型パラメータは、
「値が複数あるなら、型の“変数”も複数あっていい」という仕組み。<T, U> のように書くことで、
「T はこれ、U はこれ」というふうに、
複数の型どうしの関係を表現できる。
例えば、
2つの値をタプルにするなら <T, U>
配列の map なら <TInput, TOutput>
オブジェクトとキーなら <TObj, TKey extends keyof TObj>
のように、「それぞれの型パラメータが何を表しているか」を意識して名前をつけると、
読みやすくて壊れにくいジェネリクスになる。
まずは次の 2 つを、自分の手で書いてみてください。
function pair<T, U>(a: T, b: U): [T, U] { return [a, b]; }
function mapArray<TInput, TOutput>(arr: TInput[], fn: (v: TInput) => TOutput): TOutput[] { return arr.map(fn); }
TypeScriptそして、いろんな型の組み合わせで呼び出してみてください。
そこで、
「T と U は“別々の型の穴”で、呼び出しごとにちゃんと中身が変わるんだな」
と体で感じられたら、
複数型パラメータの基礎はもうしっかり掴めています。
