TypeScript | 関数・クラス・ジェネリクス:ジェネリクス基礎 - ジェネリクスとanyの違い

TypeScript TypeScript
スポンサーリンク

ゴール:「どっちも“なんでも受け取れる”のに、なぜジェネリクスが偉いのか」を腑に落とす

ジェネリクスも any も、一見どちらも「どんな型でも受け取れる」ように見えます。
だからこそ初心者ほどこう思いやすいです。

any でよくない?ジェネリクスって何が嬉しいの?」

ここでのゴールは、

「ジェネリクスは“型を守りながら汎用化する”、any は“型を捨てて汎用化する”」

この違いを、コードレベルでハッキリ感じられるようになることです。


まずは any から見てみる:「なんでもアリ」だけど「何も分からない」

any を使った関数の例

例えば、配列の先頭要素を返す関数を any で書いてみます。

function firstAny(arr: any[]): any {
  return arr[0];
}

const n = firstAny([1, 2, 3]);        // n: any
const s = firstAny(["a", "b", "c"]);  // s: any
TypeScript

どんな配列でも受け取れます。
一見「便利そう」に見えますよね。

でも、戻り値の型は常に any です。
つまり、コンパイラから見たら「中身の型は分からない」という状態になります。

s.toUpperCase();   // これは string っぽいから動く
s.hogehoge();      // これもコンパイルは通ってしまう(実行時に落ちるかも)
TypeScript

any を使った瞬間、
TypeScript が本来くれるはずの「型による守り」が外れてしまいます。

any は「型チェックをオフにするスイッチ」

any の本質は、

「ここは型チェックしなくていいです、と TypeScript に宣言するもの」

です。

だから、any を使うときは常に、

「ここは本当に“型チェックを捨ててでも”楽をしたい場所か?」

と自分に問いかける必要があります。

any 自体が悪ではないけれど、
多用すると「TypeScript を使っている意味」がどんどん薄れていきます。


ジェネリクス版と並べて比較する:「型を保ったまま汎用化する」

同じ関数をジェネリクスで書く

さっきの firstAny を、ジェネリクスで書き直してみます。

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

const n = first([1, 2, 3]);        // n: number | undefined
const s = first(["a", "b", "c"]);  // s: string | undefined
TypeScript

ここでのポイントは、

<T> が「型の変数」になっていることです。

呼び出しごとに、

  • first([1, 2, 3]) のときは T = number
  • first(["a", "b"]) のときは T = string

と推論されます。

つまり、

「どんな型の配列でも受け取れる汎用性」
かつ
「戻り値の型は、渡した配列の要素型にきっちり対応する」

という状態になっています。

any 版とジェネリクス版の決定的な違い

もう一度並べてみます。

function firstAny(arr: any[]): any {
  return arr[0];
}

function firstGeneric<T>(arr: T[]): T | undefined {
  return arr[0];
}
TypeScript

firstAny の戻り値は常に any
「何が返ってくるか」が型から分かりません。

firstGeneric の戻り値は「渡した配列の要素型」によって変わります。

const a = firstAny(["hello"]);
a.toUpperCase();   // OK(でも a が string だと型は保証してくれない)

const b = firstGeneric(["hello"]);
b?.toUpperCase();  // OK(b は string | undefined と分かっている)
TypeScript

ジェネリクスは、

「汎用性」と「型安全」を同時に満たすための仕組み

であり、
any は「汎用性のために型安全を捨てる」選択肢です。


もう少し複雑な例で違いを体感する

map 関数を any で書いた場合

配列の各要素を変換する map を、any で書いてみます。

function mapAny(arr: any[], fn: (value: any) => any): any[] {
  return arr.map(fn);
}

const result = mapAny([1, 2, 3], (n) => n.toString());
// result: any[]
TypeScript

result の型は any[] です。
中身が string なのか number なのか、型からは分かりません。

その後のコードで、

result[0].toUpperCase();  // コンパイルは通るが、実行時に落ちる可能性もある
TypeScript

という危険な状態になります。

map 関数をジェネリクスで書いた場合

同じものをジェネリクスで書くとこうなります。

function mapGeneric<TInput, TOutput>(
  arr: TInput[],
  fn: (value: TInput) => TOutput
): TOutput[] {
  return arr.map(fn);
}

const result2 = mapGeneric([1, 2, 3], (n) => n.toString());
// result2: string[]
TypeScript

ここでは、

  • TInput は元の配列の要素型
  • TOutput は変換後の要素型

として扱われています。

result2 の型は string[] なので、
その後のコードでも型安全が保たれます。

result2[0].toUpperCase();  // OK(string なので)
result2[0].toFixed(2);     // エラー(string に toFixed はない)
TypeScript

この「間違いをコンパイル時に止めてくれるかどうか」が、
ジェネリクスと any の一番大きな差です。


「ジェネリクスは“型の穴”」「any は“型の放棄”」

ジェネリクスは「あとから決まる型の穴」

ジェネリクスの <T> は、

「ここにはあとから具体的な型が入る“穴”」

です。

function identity<T>(value: T): T {
  return value;
}

const a = identity(1);        // T = number → a: number
const b = identity("hello");  // T = string → b: string
TypeScript

関数の中では T としか書いていませんが、
呼び出しごとに T が変わり、その情報が戻り値まできちんと伝わります。

「型の情報を保ったまま、抽象化している」のがジェネリクスです。

any は「ここは型チェックしなくていい」の宣言

一方で any は、

「ここは型チェックをやめます」という宣言

です。

function identityAny(value: any): any {
  return value;
}

const x = identityAny(1);        // x: any
const y = identityAny("hello");  // y: any

x.toFixed(2);   // コンパイルOK(実行時に落ちる可能性あり)
y.toFixed(2);   // これもコンパイルOK(string なのに)
TypeScript

ジェネリクスは「型の穴」
any は「型の放棄」

この対比で覚えておくと、使い分けの感覚がかなりクリアになります。


じゃあ any は絶対ダメなのか?実務的な視点

any が「仕方なく」役に立つ場面もある

正直に言うと、実務では any を使う場面もあります。

例えば、

  • 型定義のない外部ライブラリを一時的に扱うとき
  • どうしても型が複雑すぎて、まずは動かしたいとき
  • 段階的に型付けを進めている途中のコード

などです。

ただし、その場合でも、

「ここは今は any にしておくけど、あとでちゃんと型をつけたい」

という意識を持っておくことが大事です。

「最初から any に逃げない」が大事なマインド

ジェネリクスで書けるところを any で済ませてしまうと、
せっかく TypeScript を使っている意味が薄れてしまいます。

迷ったときの指針としては、

  • 「ロジックは同じで、型だけ違う」 → ジェネリクスを検討する
  • 「型の情報をちゃんと保ちたい」 → ジェネリクス
  • 「どうしても型が書けない/今は間に合わせたい」 → やむを得ず any

くらいの感覚でいると、バランスが取りやすいです。


まとめ:ジェネリクスと any の違いを自分の言葉で説明すると

最後に、あなた自身の言葉でこう整理してみてください。

ジェネリクスは、

「型に“変数”を導入して、あとから具体的な型を差し込める仕組み」。
どんな型でも受け取れる汎用性を持ちながら、
呼び出しごとに「正しい具体的な型」を保ってくれる。

any は、

「ここは型チェックをやめる」という宣言。
どんな型でも受け取れる代わりに、
型の情報が失われ、コンパイル時の安全性も失われる。

まずは、次の 2 つを自分の手で書いて比べてみてください。

function firstAny(arr: any[]): any { return arr[0]; }
function first<T>(arr: T[]): T | undefined { return arr[0]; }
TypeScript

そして、firstAnyfirst の戻り値に対して、
いろいろなメソッドを呼んでみてください。

そのとき、

「ジェネリクスは“守ってくれる汎用性”、any は“守りを捨てた汎用性”なんだな」

と感じられたら、このテーマはもうしっかり掴めています。

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