TypeScriptのジェネリクスの型引数名はT
をよく見かけると思いますが、その1文字を使うべきなのでしょうか? もしそうでないとしたら、名付けについてのベストプラクティスはあるのでしょうか?
本稿では、そういった疑問を元に型引数名の名付けを考えてみたいと思います。
型引数に使える文字
型引数に使える文字にはどこまでの自由と制限があるのでしょうか?見ていきましょう。
まず、型引数名には、変数名や関数名、クラス名などと同じ文字種とパターンが使えます。特定の大文字アルファベットでなければならないという縛りも、1文字でないとならないという言語仕様上の制限はありません。例えば、<A>
や3文字の<Foo>
はもちろんOKだし、キャメルケースの<fooBar>
や、記号の<$>
、ユニコードを使った<かた>
でもいいわけです。
type Test1<A> = A
type Test2<Foo> = Foo
type Test3<fooBar> = fooBar
type Test4<$> = $
type Test5<かた> = かた
逆に、数字で始まるものや、class
などの予約語は、型引数名には使用できません。
type Test6<1> = 1 // コンパイルエラー
type Test7<class> = class // コンパイルエラー
型引数名について確認された慣習
上はあくまで、型変数の名付けがどこまでの表現が許されているかの理論的な制約を示したに過ぎません。一方で、慣習的な傾向はあります。さっそくその傾向について述べたいと思いますが、後述することはTypeScriptコミュニティ全般に当てはまるというわけではなく、TypeScriptのビルトインライブラリでは確認された傾向であることはご注意ください。サードパーティ製のモジュールでは、異なった名付け方をすることもあります。
-
T
がよく使われる。 - 大文字始まりのキャメルケース。
TFunction
など。- TS本家では
T
を接頭辞にする慣習があるようです。 - スネークケース(
foo_bar
)や小文字始まりのキャメルケース(fooBar
)は一般的ではありません。
- TS本家では
- 型引数が2つの場合は
T
とU
を使う。例:Exclude<T, U>
。- アルファベットのTの次がUのため。
- 型引数が複数あり順番が重要な場合は、連番を振ることもある。
- 例
bind<A0, A1, A extends any[], R>(this: new (arg0: A0, arg1: A1, ...args: A) => R, thisArg: any, arg0: A0, arg1: A1): new (...args: A) => R
- 例
- 一部のアルファベットには意味を持つものがある。
-
K
: オブジェクトプロパティのキーのセット(keys) -
A
: 引数(argument) -
R
: 戻り値(return) -
P
: オブジェクトプロパティ(property)や引数(parameter)
-
議論
「ジェネリクスの変数名は1文字じゃないとだめなのか?」という疑問をツイッターで投げかけたところ、数多くのコメントを頂きました。ここでは寄せられた意見を取り上げていきます。
型引数名には前述したとおりの自由度があるわけですが、なぜ1文字が好まれるのでしょうか?
そのひとつの答えとして、ジェネリクスを使う場面が非常に抽象的なことがらを扱うということがあげられます。例えば、配列の型はArray<T>
と定義され、T
が要素を表します。要素はプリミティブな値かも知れないし、オブジェクトかも知れません。こうした抽象的な型を扱おうとすると、具体的な名前をつけるほうが難しくなります。
1文字に対する反論として、Array<Element>
とするほうが「要素」を明示していて良いとする主張もあります。この主張には一理ありそうです。これに対しては、実存するクラス名と見分けがつきにくいという主張もあります。
加えて、型引数の制約を表現するときに実存するクラス名と衝突してしまい、具体的な名前が書きにくいという場面もあります。次の例のError extends Error
は左が型引数名、右がECMAScriptに実存するErrorを指定するとこで、型引数Error
に代入できる型をError
型に制限する制約を意図したものですが、コンパイラは意図をくみ取ることできずにエラーになります。
type Result<Value, Error extends Error> =
{ type: "success", value: Value } |
{ type: "failure", error: Error }
// コンパイルエラー: Type parameter 'Error' has a circular constraint.(2313)
このような実存するクラス名との衝突を避けるために、T
接頭辞をつけた型引数名にするアプローチもあります。例えばTError extends Error
。
型引数を1文字にするか否かだけにこだわるべきでないという考え方もあります。型引数が具体的な意味を持っているならばそのとおりに名付ければいいし、抽象的すぎて名付けられない場合は、より抽象的な記号としてT
などを使うべきだとする考え方です。
型引数がいくつあるのかに着目し、名付けを決める方法もあります。TypeScript Deep Dive 日本語版は、複数の型引数がある場合は、意味のある名前を用いることを推奨しています。その場合、T
を接頭辞として使用するとしています。
ヒント:必要に応じてジェネリクスパラメータを呼び出すことができます。単純なジェネリックを使うときは
T
、U
、V
を使うのが普通です。複数のジェネリクス引数がある場合は、意味のある名前を使用してください。例えばTKey
とTValue
です(一般にT
を接頭辞として使用する規約は、他の言語(例えばC++)ではテンプレートと呼ばれることもあります)。
普通の変数名を決めるときと同じアプローチを適用すべきという提案もあります。変数名を決める際のひとつのテクニックとして、変数のスコープが広いほど長い名前、狭いほど短い名前にするという手法があります。例えば、for
ループの変数にi
を用いることがありますが、i
一文字を文脈なしに示されると、それが一体何を指しているのかわかりません。しかし、現実のコードでi
が出てきてもそのスコープが短ければ困ることはありません。これと同じことが、型引数でも言えるわけです。例えば、Array
のmap
メソッドはmap<U>(func: (value: T) => U): U[]
のように定義できますが、U
のスコープはメソッドの範囲のみなので、短くとも支障がないわけです。
まとめ
- 型引数名の自由度はクラス名と同等。
- 型引数名の名付けにはある程度、慣習がある。
- 型引数名に1文字にこだわる必要はないが、1文字にするのには合理的な理由がある。
- 場合によっては1文字のほうがいいし、場合によっては具体的な名前のほうがいい。
型引数名の名付けについて、みなさんの意見をお聞きできると嬉しいので、よかったらコメントください。