🔲

ジェネリクスについて聞かれたのでTypeScriptベースでなるべく分かりやすく説明してみる

2024/08/26に公開

語弊がありそうだがあえて一言で言うと?

型情報を維持するために使うもの!!

説明するよりもコード見せたほうが早いと思われるため、下記にサンプルを書いていく。

PlayGround

下記でお試しできるので、動かしたり変更したりすると良い。
https://www.typescriptlang.org/play/?#code/Q

string型の関数(ジェネリクスじゃない関数)

function aaa1(str: string): string {
    return str
}
console.log(aaa1('moji1').startsWith('m')) // true

これだと明確だし、returnされた値の型情報は維持される。

だがしかし、この処理は入ってきた値をreturnしているだけなので、これなら色々な型に対応できるはずだが、string型のみしか受け付けない書き方になってしまう。

引数に型情報を定義する関数(ジェネリクスじゃない関数その2)

function aaa2(obj: {name: string}): string {
    return obj.name
}
console.log(aaa2({name: 'moji2'}).startsWith('m')) // true
console.log(aaa2({name: 'momo', category: 'fruit'}).startsWith('m')) // true

ジェネリクスとは関係ないけど、外側にインターフェースを定義しなくても直接引数部分に書ける書き方。

TypeScriptのこの書き方は、コード量を削減できるので、めちゃイケだと思う。

any型の関数(なんでもOKな型の関数)

function aaa3(str: any): any {
    return str
}
console.log(aaa3('moji3').startsWith('m')) // true

こちらはどんな型でも入る書き方ではあるが、型情報が失われる。

なので、メソッドのstartsWithを書き間違えても型エラーとはならず、実行時エラーとなってしまう。
(型エラーのほうが、即時フィードバックとなるので良いと思われる)

余談だが、TypeScriptでは型の恩恵を受けるために出来るだけany型は避けた方が良いと言われている。
(リクエストから届くjsonなど、型情報が不明なものもあるので、厳密に全て排除できるわけではないが。。。)

ジェネリクス型の関数(なんでもあり型の関数ではあるが、型情報が維持される)

function aaa4<T>(str: T): T {
    return str
}
console.log(aaa4<string>('moji4').startsWith('m')) // true
console.log(aaa4('momo4').startsWith('m')) // 呼び出し時のジェネリクス型指定は省略化

今回のお題のジェネリクスである。

<T>の部分がジェネリクスを使う宣言となるので、ジェネリクスのTとして認識できる。
(Tという文字列は、AでもHOGEでもなんでも良い。他の型と被らないように1文字が良く見かける書き方。)

<T>の部分を書いてあげないとトランスパイラ側は、型情報のTがあるはずと検索してしまいエラーとなる。
(Tというクラスや型を宣言していればエラーとならないが、それだと普通の関数となる)

ジェネリクスを使うと型情報は維持されるし、どんな型でも受け入れられるという良いとこ取りができる。

アーマードコア6のフロイト風に言うと、「型情報が維持されているな。そういう動きだ。」という感じかと。

【余談】ジェネリクスにTという文字が使われることが多いがなんなの?

ジェネリクスのサンプルでTの文字が使われることが多いが、Typeの頭文字を取って使っているらしい。

Mapクラスのジェネリクスだと、KVを使っていて、KeyとValueの頭文字だと思われる。

ElementならEとするなど、ジェネリクスに使う文字列は扱う対象の頭文字を使うという作法なのかもしれない。

個人的には分かりやすい作法だと思うが、Tが使われる理由については説明されるまで気づかない話だと思う。

型を2種類使ったサンプル

function aaa5<X, Z>(_str: X, day: Z): Z {
    return day
}
console.log(aaa5('moji5', new Date()).getFullYear()) // 2024

こちらは、Date型をreturnしているので問題なく動く。

試しに、return型の部分をXに変更すると、型エラーが表示されるので試してみてほしい。

あまり意味ないバージョン

function aaa6<A>(str: A): void {
    console.log(str) // moji6
}
aaa6('moji6')

動きはするが、このパターンはジェネリクスにしても旨味の無いパターンだと思われる。

型の範囲を狭めるバージョン

interface Hoge {
  name: string;
}
function aaa7<T extends Hoge>(hoge: T): void {
    console.log(hoge.name) // moji7
}
aaa7({name: 'moji7'})

引数に型情報を定義する関数 とやっていることは同じ。

すでに型情報が定義されているのを使いたいが、その型に限定したくない時に使う感じかと。
(特定の型を指定すると、その型以外受付なくなってしまうため)

まとめ

そんなに難しい話じゃないが、なんか難しく感じるのがジェネリクスだなと思った。

他の見方

https://typescriptbook.jp/reference/generics

上記によると下記らしい。

  • ジェネリクスは、コードの共通化と型の安全性を両立するための言語機能。
  • ジェネリクスは、型も引数のように扱うという発想。

ふむ。

Discussion