TypeScript | 関数・クラス・ジェネリクス:ジェネリクス基礎 - 複数型パラメータ

TypeScript TypeScript
スポンサーリンク

ゴール:「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;
}
TypeScript

U がまったく使われていません。

型パラメータは「増やせば偉い」ものではなく、
「必要な分だけ使う」のが正解です。

「この型パラメータは、本当に別の型として扱う必要があるか?」
「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 は“別々の型の穴”で、呼び出しごとにちゃんと中身が変わるんだな」

と体で感じられたら、
複数型パラメータの基礎はもうしっかり掴めています。

タイトルとURLをコピーしました