TypeScript / LESSON 10 / TS10

TypeScript:ジェネリクス入門

1学習の目的

2基礎解説

「配列の最初の要素を返す関数」を作りたいとします。数値配列・文字列配列・オブジェクト配列——すべてに対応したいが、any は使いたくない。そこでジェネリクスの出番です。

やりたいこと書き方意味
型を引数にするfunction f<T>(x: T): TTは呼び出し時に決まる
複数の型引数function f<A, B>(a: A, b: B)いくつでも書ける
型に制約を付ける<T extends { length: number }>length を持つ型だけ
ジェネリックな型type Box<T> = { value: T }型自体を部品化
✅ 覚えるべき重要ポイント
  1. <T> は「型の変数」。関数を呼ぶときに実際の型が決まる。first([1,2,3]) なら T は number、first(["a"]) なら string になる。
  2. any との決定的な違いは、型情報が失われないこと。any だと戻り値も any になるが、ジェネリクスなら「入れた型がそのまま出てくる」。
  3. Tという名前に意味はない。慣習的に T(Type)・K(Key)・V(Value)が使われるが、<Item> のように分かりやすい名前でもよい。
  4. extends で制約を付けられる<T extends { length: number }> と書けば、length を持つ型(文字列・配列など)だけを受け付ける。
  5. 型自体もジェネリックにできるBox<number>Box<string> のように、中身の型を後から指定する型を作れる。

現場使用例:APIレスポンスの共通ラッパー、配列操作のユーティリティ関数、状態管理ライブラリ、フォームの汎用コンポーネント。「型が違うだけで処理は同じ」という場面はすべて対象になる

// ❌ any だと型情報が失われる
function firstAny(arr: any[]): any {
  return arr[0];
}
const a = firstAny([1, 2, 3]);   // a は any(number ではない)

// ✅ ジェネリクスなら型が保たれる
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}
const n = first([1, 2, 3]);      // number | undefined
const s = first(["a", "b"]);     // string | undefined

// 複数の型引数
function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}
console.log(pair("x", 1));   // ['x', 1]

// 制約を付ける(length を持つ型だけ)
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}
console.log(longest("abc", "ab"));      // "abc"
console.log(longest([1, 2], [1, 2, 3])); // [1,2,3]

// 型自体をジェネリックにする
type Box<T> = { value: T; label: string };

const numBox: Box<number> = { value: 42, label: "数値" };
const strBox: Box<string> = { value: "あ", label: "文字" };

3基本ドリル(10問)

TS10-D01 配列の最初の要素を返すジェネリック関数 first を定義し、数値配列と文字列配列の両方で試して2行出力せよ。 コーディング★☆☆無料
期待される結果

出力2行。1 / a

ヒント

function first(arr: T[]): T | undefined { return arr[0]; } と書く。

模範解答
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

console.log(first([1, 2, 3]));
console.log(first(["a", "b"]));
解説

T は呼び出すときに決まる。数値配列を渡せば T は number、文字列配列なら string になり、戻り値の型もそれに追随する。

TS10-D02 ジェネリクスと any の決定的な違いはどれか。
A. 速度が速い B. 型情報が失われない C. 記述が短い D. 違いはない
選択★☆☆無料
期待される結果

解答 == B

ヒント

any を使うと戻り値の型はどうなるか。

模範解答
B
解説

any は戻り値も any になり、その後の処理で型チェックが効かなくなる。ジェネリクスなら「入れた型がそのまま出てくる」ので安全性が保たれる。

TS10-D03 2つの値を受け取り、順序を入れ替えた配列を返すジェネリック関数 swap を定義せよ(型引数は2つ)。swap("a", 1) の結果を出力すること。 コーディング★☆☆無料
期待される結果

出力 == 1,a

ヒント

function swap(a: A, b: B): [B, A] { return [b, a]; } と書く。

模範解答
function swap<A, B>(a: A, b: B): [B, A] {
  return [b, a];
}

console.log(swap("a", 1).join(","));
解説

型引数は複数書ける。戻り値で順序を入れ替えていることも、型として [B, A] と正確に表現されている。

TS10-D04 ジェネリック関数の型引数を補ってコードを完成させよ。 穴埋め★☆☆無料
コード
function last<____>(arr: T[]): T | undefined {
  return arr[arr.length - 1];
}

console.log(last([1, 2, 3]));
期待される結果

出力 == 3

ヒント

型引数の名前。慣習的に使われる大文字1文字。

模範解答
T
解説

T という名前に特別な意味はない。Type の頭文字を取った慣習で、<Item> のような分かりやすい名前でも構わない。

TS10-D05 function f<T extends { length: number }>(x: T) はどんな型を受け付けるか。
A. すべての型 B. length を持つ型だけ C. 数値だけ D. オブジェクトだけ
選択★☆☆無料
期待される結果

解答 == B

ヒント

extends は制約を表す。

模範解答
B
解説

文字列や配列は length を持つので渡せるが、数値は渡せない。制約を付けることで、T に対して安全に x.length が使えるようになる。

TS10-D06 length を持つ2つの値を受け取り、長いほうを返す関数 longest を定義せよ(制約を使うこと)。文字列と配列の両方で試し、2行出力すること。 コーディング★★☆無料
期待される結果

出力2行。abcde / 1,2,3

ヒント

と制約を付けると、a.length が使える。

模範解答
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

console.log(longest("abcde", "abc"));
console.log(longest([1, 2], [1, 2, 3]).join(","));
解説

制約が無ければ a.length でエラーになる。「T は length を持つ」と伝えることで、初めてそのプロパティが使える。

TS10-D07 型に制約を付けるキーワードを補ってコードを完成させよ。 穴埋め★★☆無料
コード
function size<T ____ { length: number }>(x: T): number {
  return x.length;
}

console.log(size("hello"));
期待される結果

出力 == 5

ヒント

「〜を満たす」を意味する7文字のキーワード。継承と同じ単語。

模範解答
extends
解説

ジェネリクスの extends は「継承」ではなく「制約」を意味する。「この条件を満たす型だけを受け付ける」という指定になる。

TS10-D08 type Box<T> = { value: T } と定義したとき、Box<number> の value の型はどれか。
A. T B. number C. any D. unknown
選択★★☆無料
期待される結果

解答 == B

ヒント

T に number を指定している。

模範解答
B
解説

型引数に具体的な型を渡すと、その型で置き換わるBox<string> なら value は string になる。

TS10-D09 ジェネリックな型 Box(value と label を持つ)を定義し、数値用と文字列用の2つを作って「数値:42」「文字:あ」の形式で2行出力せよ。 コーディング★★☆無料
期待される結果

出力2行。数値:42 / 文字:あ

ヒント

type Box = { value: T; label: string }; と定義し、Box と Box を作る。

模範解答
type Box<T> = {
  value: T;
  label: string;
};

const numBox: Box<number> = { value: 42, label: "数値" };
const strBox: Box<string> = { value: "あ", label: "文字" };

console.log(`${numBox.label}:${numBox.value}`);
console.log(`${strBox.label}:${strBox.value}`);
解説

型そのものを部品化できる。同じ構造で中身の型だけ違うデータを、1つの型定義で表現できる。

TS10-D10 配列と変換関数を受け取り、変換後の配列を返すジェネリック関数 mapAll を定義せよ(型引数は2つ)。[1,2,3] を文字列に変換して出力すること。 コーディング★★☆無料
期待される結果

出力 == 1件,2件,3件

ヒント

function mapAll(arr: T[], fn: (item: T) => U): U[] と書く。

模範解答
function mapAll<T, U>(arr: T[], fn: (item: T) => U): U[] {
  return arr.map(fn);
}

console.log(mapAll([1, 2, 3], (n) => `${n}件`).join(","));
解説

入力の型 T と出力の型 U を別々に表現している。標準の map もこれと同じ仕組みで型が付いており、JS09で使ったメソッドの内側が見えてくる。

4実践シナリオ(5問)

TS10-S01 【安全な取り出し】配列と番号を受け取り、範囲外なら既定値を返すジェネリック関数 at を定義せよ。[10,20,30] の 1番目と 99番目(既定値0)で試し、2行出力すること。 コーディング★★☆無料
期待される結果

出力2行。20 / 0

ヒント

function at(arr: T[], index: number, fallback: T): T と書く。範囲外なら fallback を返す。

模範解答
function at<T>(arr: T[], index: number, fallback: T): T {
  return arr[index] ?? fallback;
}

console.log(at([10, 20, 30], 1, 0));
console.log(at([10, 20, 30], 99, 0));
解説

既定値の型も T なので、配列と違う型を渡そうとするとエラーになる。数値配列に文字列の既定値、という不整合が起きない。

TS10-S02 【APIレスポンスの共通型】ジェネリックな Result 型(ok: データあり / err: メッセージあり)を定義し、データを取り出すか既定値を返す関数 unwrap を作れ。成功(数値10)と失敗(文字列の既定値「なし」)で試し、2行出力すること。 コーディング★★☆無料
期待される結果

出力2行。10 / なし

ヒント

type Result = { kind: "ok"; data: T } | { kind: "err"; message: string }; と定義する。

模範解答
type Result<T> =
  | { kind: "ok"; data: T }
  | { kind: "err"; message: string };

function unwrap<T>(r: Result<T>, fallback: T): T {
  return r.kind === "ok" ? r.data : fallback;
}

console.log(unwrap<number>({ kind: "ok", data: 10 }, 0));
console.log(unwrap<string>({ kind: "err", message: "失敗" }, "なし"));
解説

TS09のタグ付きユニオンをジェネリックにした形。どんな型のデータでも、同じ成功/失敗の仕組みで扱える——実務のAPIラッパーはこの形が定番。

TS10-S03 【重複の除去】配列から重複を取り除くジェネリック関数 unique を定義せよ。数値配列 [1,2,2,3,1] と文字列配列 ["a","b","a"] で試し、2行出力すること。 コーディング★★☆無料
期待される結果

出力2行。1,2,3 / a,b

ヒント

Set を使うと簡潔に書ける。[...new Set(arr)] で重複が消える。

模範解答
function unique<T>(arr: T[]): T[] {
  return [...new Set(arr)];
}

console.log(unique([1, 2, 2, 3, 1]).join(","));
console.log(unique(["a", "b", "a"]).join(","));
解説

型が違っても処理は同じ——まさにジェネリクスが活きる場面。any で書けば動くが、戻り値が any[] になり、その後の処理で型が守られなくなる。

TS10-S04 【グループごとの件数】配列とキー取得関数を受け取り、キーごとの件数を返すジェネリック関数 countBy を定義せよ。商品配列(カテゴリ PC/周辺機器/PC)で試し、「PC:2, 周辺機器:1」と出力すること。 コーディング★★★無料
期待される結果

出力 == PC:2, 周辺機器:1

ヒント

戻り値の型は Record。ループで加算していく。

模範解答
function countBy<T>(arr: T[], key: (item: T) => string): Record<string, number> {
  const result: Record<string, number> = {};

  for (const item of arr) {
    const k = key(item);
    result[k] = (result[k] ?? 0) + 1;
  }

  return result;
}

const products = [
  { name: "ノートPC", category: "PC" },
  { name: "マウス", category: "周辺機器" },
  { name: "モニター", category: "PC" },
];

const counts = countBy(products, (p) => p.category);

console.log(
  Object.entries(counts)
    .map(([k, v]) => `${k}:${v}`)
    .join(", ")
);
解説

キーの取り出し方を関数として外から渡しているので、どんなデータでも集計できる。Record<string, number> は「文字列をキーに数値を持つオブジェクト」を表す型。

TS10-S05 【IDによる検索】id を持つ配列から指定IDの要素を探すジェネリック関数 findById を定義せよ(制約で id: number を持つ型に限定すること)。商品配列で id=2 と id=99 を試し、「マウス」「該当なし」と2行出力すること。 コーディング★★★無料
期待される結果

出力2行。マウス / 該当なし

ヒント

と制約を付ける。find の結果は undefined になりうる。

模範解答
function findById<T extends { id: number }>(
  items: T[],
  id: number
): T | undefined {
  return items.find((item) => item.id === id);
}

const products = [
  { id: 1, name: "ノートPC" },
  { id: 2, name: "マウス" },
];

for (const id of [2, 99]) {
  const found = findById(products, id);
  console.log(found?.name ?? "該当なし");
}
解説

制約により id を持たない型は渡せない。それでいて戻り値は元の型そのまま(Product)なので、found.name のように固有のプロパティにアクセスできる。

5仕上げ課題

TS10-FINAL 【汎用データ処理ライブラリ】型を問わず使えるユーティリティ関数群を実装せよ。

作る関数(すべてジェネリック)
first<T>(arr: T[]): T | undefined … 最初の要素
unique<T>(arr: T[]): T[] … 重複を除去
sumBy<T>(arr: T[], fn: (item: T) => number): number … 数値を取り出して合計
groupBy<T>(arr: T[], key: (item: T) => string): Record<string, T[]> … キーごとに分類

データ:商品配列
・{ name: "ノートPC", category: "PC", price: 128000 }
・{ name: "マウス", category: "周辺機器", price: 3200 }
・{ name: "モニター", category: "PC", price: 45000 }
・{ name: "キーボード", category: "周辺機器", price: 8500 }

出力仕様(5行)
最初の商品:ノートPC
カテゴリ:PC, 周辺機器
合計金額:184,700円
PC:ノートPC, モニター
周辺機器:マウス, キーボード
期待される結果

出力5行が完全一致

ヒント

groupBy は Record を返す。Object.entries で回すと [キー, 配列] のペアが得られる。カテゴリ一覧は map と unique の組み合わせで作れる。

模範解答
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

function unique<T>(arr: T[]): T[] {
  return [...new Set(arr)];
}

function sumBy<T>(arr: T[], fn: (item: T) => number): number {
  return arr.reduce((sum, item) => sum + fn(item), 0);
}

function groupBy<T>(
  arr: T[],
  key: (item: T) => string
): Record<string, T[]> {
  const result: Record<string, T[]> = {};

  for (const item of arr) {
    const k = key(item);
    if (!result[k]) {
      result[k] = [];
    }
    result[k].push(item);
  }

  return result;
}

const products = [
  { name: "ノートPC", category: "PC", price: 128000 },
  { name: "マウス", category: "周辺機器", price: 3200 },
  { name: "モニター", category: "PC", price: 45000 },
  { name: "キーボード", category: "周辺機器", price: 8500 },
];

console.log(`最初の商品:${first(products)?.name ?? "なし"}`);

const categories = unique(products.map((p) => p.category));
console.log(`カテゴリ:${categories.join(", ")}`);

const total = sumBy(products, (p) => p.price);
console.log(`合計金額:${total.toLocaleString()}円`);

const grouped = groupBy(products, (p) => p.category);

for (const [category, items] of Object.entries(grouped)) {
  console.log(`${category}:${items.map((i) => i.name).join(", ")}`);
}
解説

4つの関数はすべて「商品」を一切知らない。それでも商品配列を正しく処理できている——これがジェネリクスの威力だ。

もし any で書いていたらどうなるか。first(products) の戻り値は any になり、first(products).nmae のような打ち間違いも素通りする。汎用性と安全性は両立できるということを、この4つの関数が示している。

とくに groupBy の戻り値 Record<string, T[]> に注目してほしい。「文字列のキーに、T の配列が対応する」——商品を渡せば Record<string, Product[]> になるので、items.map((i) => i.name) と書いたときに i が Product だと分かる。型が最後まで追跡されている。

実務で使うライブラリ(lodash、React、状態管理ツール)は、ほぼすべてこの仕組みで作られている。ジェネリクスが読めるようになると、ライブラリの型定義が読めるようになり、使い方を推測できるようになる。

次章では配列操作と型を扱う。map・filter・reduce に、実際にどう型が付いているのかを見ていく。