ゴール:「T なのに T | U もアリ?を“安全に混ぜる感覚”で理解する
ジェネリクスと union(|)を一緒に使うとき、
最初に出てくるモヤモヤはだいたいこれです。
「T って“1つの型”じゃないの?T | U みたいに“複数の型”とどう共存するの?」
ここでのゴールは、
「ジェネリクスは“型の穴”、union は“型の候補の集合”
それをどう組み合わせると“柔軟だけど安全な API”になるか
を、コードレベルで腑に落とすことです。
基本イメージ:T は「穴」、union は「候補のセット」
まずは union 単体のイメージを整理する
union 型は、こういうやつです。
type Id = number | string;
let id: Id;
id = 1;
id = "user-1";
// id = true; // エラー
TypeScriptnumber | string は、
「number か string のどちらか」
という“候補の集合”です。
この時点では、まだジェネリクスは出てきません。
ただ、
「1つの変数に、複数の型の可能性を持たせる」
という感覚だけ持っておいてください。
ジェネリクスと union を組み合わせるとどうなるか
例えば、こういうジェネリック型を考えます。
type ApiResult<T> = {
success: true;
data: T;
} | {
success: false;
error: string;
};
TypeScriptここでは、
- 外側に union(成功パターン or 失敗パターン)
- 内側にジェネリクス(成功時の data の型が T)
という構造になっています。
使うときはこうです。
type User = { id: number; name: string };
const ok: ApiResult<User> = {
success: true,
data: { id: 1, name: "Taro" },
};
const ng: ApiResult<User> = {
success: false,
error: "Unauthorized",
};
TypeScriptここでのポイントは、
「ジェネリクス(T)は“成功時の data の型”という穴」
「union は“成功 or 失敗”というパターンの集合」
として役割分担していることです。
ジェネリクスと union は、
「どっちか一方だけ使うもの」ではなく、
“役割の違うレイヤー”として重ねて使うイメージです。
パターン1:T 自体を union にする
「T は number か string のどちらか」というジェネリクス
ジェネリクスの型パラメータ自体を union にすることもできます。
function toArray<T>(value: T): T[] {
return [value];
}
const a = toArray<number | string>(1); // (number | string)[]
a.push("hello");
TypeScriptここでは、
- T =
number | string - 戻り値の型は
(number | string)[]
という関係です。
重要なのは、
「T は“1つの型”というより、“1つの型表現”」
だということです。
その“1つの型表現”が、number でもいいし、number | string でもいい。
ジェネリクスは「型の穴」であって、
「必ず単一のプリミティブ型でなければならない」わけではありません。
セキュリティ視点での意味
T = number | string のように union を使うと、
「この配列には number と string 以外は入らない」
という制約が型レベルでかかります。
any[] だと「何でも入る」状態ですが、(number | string)[] だと「許可された型だけが入る」状態です。
これは、セキュリティでいう ホワイトリスト に近い考え方です。
パターン2:ジェネリクスの中で union を返す
「成功時は T、失敗時はエラー」の関数
例えば、何かを検証する関数を考えます。
type ValidationResult<T> =
| { ok: true; value: T }
| { ok: false; error: string };
function validateNumber(input: string): ValidationResult<number> {
const n = Number(input);
if (Number.isNaN(n)) {
return { ok: false, error: "Not a number" };
}
return { ok: true, value: n };
}
TypeScriptここでは、
ValidationResult<T>が「T かエラー」の unionvalidateNumberはValidationResult<number>を返す
という構造です。
さらに汎用化して、ジェネリクス関数にしてみます。
function validate<T>(
input: string,
parser: (text: string) => T | null
): ValidationResult<T> {
const result = parser(input);
if (result === null) {
return { ok: false, error: "Invalid input" };
}
return { ok: true, value: result };
}
TypeScript使うときはこうです。
const r1 = validate<number>("123", (text) => {
const n = Number(text);
return Number.isNaN(n) ? null : n;
});
const r2 = validate<Date>("2024-01-01", (text) => {
const d = new Date(text);
return Number.isNaN(d.getTime()) ? null : d;
});
TypeScriptここでのポイントは、
「ジェネリクス(T)は“成功時の値の型”」
「union は“成功 or 失敗”という結果のバリエーション」
という役割分担です。
セキュリティ的に言えば、
- 入力をパースする
- 成功したら“安全な型 T”として扱う
- 失敗したらエラーとして扱う
という“安全な入力処理”の型表現になっています。
パターン3:union の中でジェネリクスを使う(判別可能 union)
判別プロパティ+ジェネリクス
セキュリティ的なイベントログを考えてみます。
type LoginEvent<TUserId> =
| { type: "success"; userId: TUserId; ip: string }
| { type: "failure"; reason: string; ip: string };
TypeScriptここでは、
typeが"success"or"failure"の判別プロパティuserIdの型だけジェネリクス(TUserId)
という構造です。
使うときはこうです。
type NumericLoginEvent = LoginEvent<number>;
type StringLoginEvent = LoginEvent<string>;
TypeScript処理側はこう書けます。
function handleLoginEvent<TUserId>(event: LoginEvent<TUserId>) {
if (event.type === "success") {
// event.userId: TUserId
// event.ip: string
} else {
// event.reason: string
// event.ip: string
}
}
TypeScriptここでのポイントは、
「union の“形”は固定しつつ、一部のフィールドの型だけジェネリクスで差し替える」
という設計です。
これにより、
- イベントの構造(type, ip, reason など)は統一
- userId の表現(number, string, UUID など)はシステムごとに変えられる
という柔軟さと一貫性を両立できます。
パターン4:ジェネリクス+union+制約(extends)
「T はこの union のどれかに限る」
ジェネリクスに union を使った制約もよく出てきます。
type Role = "admin" | "user" | "guest";
function requireRole<T extends Role>(role: T) {
// ...
}
requireRole("admin"); // OK
requireRole("user"); // OK
// requireRole("root"); // エラー
TypeScriptここでは、
Roleが union(許可されたロールの集合)T extends Roleが「T はそのどれかに限る」という制約
です。
さらに、戻り値にジェネリクスを絡めることもできます。
function canAccessAdmin<T extends Role>(role: T): boolean {
return role === "admin";
}
TypeScriptセキュリティ的には、
「許可された値の集合(union)を定義し、
ジェネリクス+制約で“その中のどれか”に限定する」
という、かなり堅い設計になります。
ありがちなつまずきポイントと考え方
「T | U」と「T が union」の違いがごちゃごちゃになる
例えば、次の 2 つは別物です。
// 1: 戻り値が「T か U」
function f1<T, U>(value: T): T | U {
// ...
throw new Error();
}
// 2: T 自体が「A か B」
function f2<T>(value: T): T {
return value;
}
const x = f2<"A" | "B">("A"); // T = "A" | "B"
TypeScript整理すると、
T | Uは「2つの型パラメータの union」T = A | Bは「1つの型パラメータに union 型を入れている」
という違いです。
どちらも「union とジェネリクスの併用」ですが、
「何を可変にしたいのか(型パラメータの数/union の中身)」を意識すると整理しやすくなります。
「union だから何でもできる」は間違い
number | string だからといって、number のメソッドも string のメソッドも自由に呼べるわけではありません。
function logId(id: number | string) {
// id.toFixed(2); // エラー
// id.toUpperCase(); // エラー
console.log(String(id)); // OK
}
TypeScriptunion 型に対してできることは、
- 両方に共通する操作
- 型ガードで絞り込んだあとの操作
だけです。
ジェネリクスと union を組み合わせるときも同じで、
「T が union だからといって、T の中の全パターンのメソッドを無条件に呼べるわけではない」
という制約は常に意識しておく必要があります。
まとめ:ジェネリクスと union の併用を自分の言葉で説明すると
最後に、あなた自身の言葉でこう整理してみてください。
ジェネリクスは「型の穴」、
union は「型の候補の集合」。
これを組み合わせると、
- 「成功時は T、失敗時はエラー」のような安全な結果型
- 「イベントの形は同じだが、一部のフィールドの型だけ差し替え可能」な設計
- 「許可された値の集合(union)からしか選べない」制約付きジェネリクス
などを表現できる。
まずは次の 3 つを、自分の手で書いてみてください。
type ApiResult<T> = { success: true; data: T } | { success: false; error: string };
function toArray<T>(value: T): T[] { return [value]; }
type LoginEvent<TUserId> =
| { type: "success"; userId: TUserId; ip: string }
| { type: "failure"; reason: string; ip: string };
TypeScriptそして、T に単一型と union 型の両方を入れてみて、
型推論の結果をエディタで眺めてみてください。
そこで、
「ジェネリクスは“穴”、union は“候補のセット”。
それを重ねると、柔軟だけど境界がちゃんと守られた設計になる。」
と感じられたら、
ジェネリクスと union の併用はもうしっかり掴めています。
