ゴール:「T って何?を“意味のある名前”にできるようになる」
ジェネリクスを書き始めると、まず最初に出てくる疑問がこれです。
「T って、なんでみんな T って書いてるの?U とか K とか、あれ何?」
ここで大事なのは、
「型パラメータにも“意味のある名前”をつける」
という感覚です。
T だけが正解ではありません。
ただし、ある程度の“お作法”があるので、それを知っておくと読み書きがかなり楽になります。
一番基本の型パラメータ名:T
「Type の T」だと思っておけばOK
まず、いちばんよく見るのが T です。
function identity<T>(value: T): T {
return value;
}
TypeScriptこれはもう「Type の T」と覚えてしまって構いません。
意味としては、
「ここには“なんらかの型”が入る。
特に名前をつけるほどの文脈はないけど、
とにかく“1つの型”として扱いたい。」
くらいのニュアンスです。
例えば、配列の先頭を返す関数もそうです。
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
TypeScriptここでの T は「配列の要素型」です。
このレベルなら、T で十分です。
「とにかく 1 種類の型を表す変数」として使っているだけなので、
わざわざ長い名前にする必要はありません。
2つ目以降の型パラメータ:U, V など
「T と“別の型”」を表したいとき
型パラメータが 2 つ以上になるとき、
よく出てくるのが U, V です。
function pair<T, U>(a: T, b: U): [T, U] {
return [a, b];
}
TypeScriptここでは、
Tは第1引数の型Uは第2引数の型
という意味です。
T と U に特別な意味があるわけではなく、
「T とは別の型だよ」ということを示すための記号
だと思ってください。
もし 3 つ目が必要なら V、
それ以上なら TKey, TValue のように、
意味を持たせた名前にしていくことが多いです。
コレクション系でよく使う命名:TItem, TElement など
「中身の型」が分かる名前にする
配列やリスト、Box のような「何かを入れる型」を扱うときは、T よりも少し意味のある名前にすると読みやすくなります。
class Box<TItem> {
constructor(private value: TItem) {}
getValue(): TItem {
return this.value;
}
}
TypeScriptTItem と書くことで、
「この型パラメータは“中に入っているアイテムの型”なんだな」
と一目で分かります。
同じように、TElement, TValue などもよく使われます。
function mapArray<TElement, TResult>(
arr: TElement[],
fn: (value: TElement) => TResult
): TResult[] {
return arr.map(fn);
}
TypeScriptここでは、
TElementは元の配列の要素型TResultは変換後の要素型
という意味です。
「T だけだと分かりにくいと感じたら、
T に“意味のあるサフィックス”をつける」
というのが、実務でよくやる工夫です。
オブジェクト系でよく使う命名:TKey, TValue, TRecord など
キーと値を区別したいとき
オブジェクトを扱うジェネリクスでは、K や V、それに TKey, TValue がよく出てきます。
function getProp<TObj, TKey extends keyof TObj>(
obj: TObj,
key: TKey
): TObj[TKey] {
return obj[key];
}
TypeScriptここでは、
TObjは「オブジェクト全体の型」TKeyは「そのオブジェクトのキーの型」
という意味です。
TObj と TKey のように、
「何の型なのか」が名前から分かるようにしておくと、
あとから読んだときの理解コストがかなり下がります。
例えば、マップ的な型ならこうも書けます。
type MapLike<TKey, TValue> = {
get(key: TKey): TValue | undefined;
set(key: TKey, value: TValue): void;
};
TypeScriptK / V だけでも通じますが、TKey / TValue の方が、
「これはキーの型」「これは値の型」と一目で分かります。
制約付きジェネリクスでの命名:T extends 〜 の T
「何のグループの中の T なのか」を名前に乗せる
制約をつけるときも、名前に意味を持たせると読みやすくなります。
function getLength<TValue extends { length: number }>(
value: TValue
): number {
return value.length;
}
TypeScriptここで TValue としておくと、
「これは“値の型”で、length を持つものなんだな」
と分かります。
もし T だけだと、
function getLength<T extends { length: number }>(value: T): number { ... }
TypeScriptとなり、「T が何者か」はコードをよく読まないと分かりません。
「extends の右側を見て、“これは何の T か”を名前に反映する」
という意識を持つと、命名の質が一段上がります。
実務での現実的なルールまとめ
僕が実際に使っている感覚的ルール
厳密な“公式ルール”というより、
現場でよく使われている「いい感じの慣習」をまとめると、こんな感じです。
単純な 1 型パラメータなら T で十分
function identity<T>(value: T): T {
return value;
}
TypeScript「とにかく 1 つの型を表すだけ」であれば、T でシンプルに書く方が読みやすいです。
2 つ目以降は U, V か、意味のある名前
function pair<T, U>(a: T, b: U): [T, U] { ... }
TypeScriptあるいは、意味を持たせてこうも書けます。
function pair<TLeft, TRight>(left: TLeft, right: TRight): [TLeft, TRight] { ... }
TypeScriptどちらがいいかは、
「その関数の文脈で、どれだけ意味を持たせたいか」で決めます。
コレクション系は TItem / TElement / TValue など
class Box<TItem> { ... }
function mapArray<TElement, TResult>(...) { ... }
TypeScript「中身の型」であることが分かる名前にすると、
後から読んだときに迷いません。
オブジェクト系は TObj / TKey / TValue が鉄板
function getProp<TObj, TKey extends keyof TObj>(obj: TObj, key: TKey): TObj[TKey] { ... }
TypeScriptT, K だけよりも、TObj, TKey の方が圧倒的に読みやすいです。
失敗しがちなパターンと、どう直すか
なんでもかんでも T, U, V にしてしまう
例えば、こんなコード。
function doSomething<T, U, V>(a: T, b: U, c: V): void {
// ...
}
TypeScript中身を見ないと、
T/U/V が何を表しているのかまったく分かりません。
これを少しだけマシにすると、こうなります。
function doSomething<TUser, TConfig, TResult>(
user: TUser,
config: TConfig,
initial: TResult
): TResult {
// ...
}
TypeScript「完璧な名前」を目指す必要はありませんが、
「少なくとも、何の種類の型なのか」は名前から分かるようにする
という意識を持つと、
自分も他人もだいぶ楽になります。
まとめ:型パラメータの命名規則を自分の言葉で整理すると
最後に、あなた自身の言葉でこう整理してみてください。
型パラメータは、ただの記号ではなく「型の変数の名前」。
だからこそ、値の変数と同じように「意味のある名前」をつけると読みやすくなる。
シンプルな 1 つの型なら T で十分。
2 つ目以降は U, V か、TKey, TValue のように意味を持たせる。
コレクションなら TItem / TElement、
オブジェクトなら TObj / TKey / TValue が定番。
今書いているジェネリック関数やクラスの型パラメータを、
一度全部眺めてみてください。
「この T、もう少し意味のある名前にできるな」と思ったところを、
1 箇所だけでもいいのでリネームしてみる。
その小さな修正を繰り返すうちに、
「型パラメータの命名センス」は、確実に育っていきます。
