お疲れ。TypeScript全盛の昨今だけど、あえて「純粋なJavaScript(JSDoc)」でガチガチの型安全なコードを書く現場に放り込まれて、頭を抱えていないか?
「TypeScriptを導入するビルドステップを入れる余裕がないレガシーな環境だけど、VS Codeの強力なIntelliSense(入力補完)の恩恵は受けたい」
「ライブラリのコア部分をVanilla JSで書きつつ、使う側には完璧な型補完を提供したい」
そんなシチュエーションで、中級から一皮むけてシニアの領域に踏み込むための強力な武器が、今回解説する JSDocの `@template` によるジェネリクス表現 だ。
TypeScriptのコンパイラ(`tsc`)やVS Codeの言語サーバー(tsserver)は、JSDocのコメントを裏側で完璧に解析している。つまり、JavaScriptの動的な柔軟性を保ったまま、TypeScriptと同等の型安全性をJSDocでエディタに強制できるってわけだ。
さあ、現場のリアルなノウハウを交えながら、その深淵を覗いていこうか。
—
なぜJSDocでジェネリクス(`@template`)が必要なのか?
JavaScriptで汎用的なユーティリティ関数を書くとき、こんな悩みに出くわしたことはないか?
「引数に渡したオブジェクトの構造をそのまま維持したまま、特定のプロパティだけをラップする関数を作りたい。でも、戻り値の型が `any` や汎用的な `object` に落ちてしまい、エディタがプロパティ名をサジェストしてくれない……」
型を固定してしまうとコードが硬直し、かといって `any` に逃げるとバグの温床になる。このジレンマを鮮やかに解決するのが、型をパラメータ化する「ジェネリクス」だ。
TypeScriptファイル(`.ts`)なら `
—
現場で即効性のある実践コード:オレオレ型安全ユーティリティ
百聞は一見にしかずだ。実務でよくある「APIから取得したキャッシュデータをラップし、タイムスタンプを付与して返す関数」を例に取ろう。
以下のコードをそのまま `.js` ファイルとしてVS Codeで開いてみてほしい。抜群の補完が効くはずだ。
/
- @file cacheUtils.js
- @description JSDocのジェネリクスを活用した型安全なキャッシュラッパー
/
/
- 任意のデータにメタ情報(タイムスタンプ)を付与してラップする関数
- @template T
- @param {T} data – ラップしたい元のデータ(どんな型でも受け付ける)
- @returns {{ data: T, cachedAt: number, isExpired: boolean }} ラップされたオブジェクト
/
function createCacheEntry(data) {
return {
data: data,
cachedAt: Date.now(),
isExpired: false
};
}
// — 使用例と恩恵の確認 —
// 1. プリミティブな値を渡した場合
const userCountCache = createCacheEntry(42);
// userCountCache.data は「number」型として推論されるため、数値メソッドが補完される
// 2. 複雑なオブジェクトを渡した場合
const userProfileCache = createCacheEntry({
id: “usr_9981”,
name: “Yamada Taro”,
roles: [“admin”, “editor”]
});
// ここで!userProfileCache.data. を打つと、id, name, roles が完璧にサジェストされる
console.log(userProfileCache.data.name);
どうだ? `.ts` ファイルを1行も作っていないのに、VS Code上で完璧な型推論が働いているのが分かるはずだ。裏側でtsserverが動いており、`T` というプレースホルダーが実引数の型に動的に置き換わっている。
—
さらに一歩進む:複数の型パラメータと制約(Constraint)の掛け合わせ
実務では、1つの型パラメータじゃ物足りない場面が多い。例えば、「キーと値のペアを受け取り、特定の変換を施す関数」などだ。
ここで `@template` の真骨頂、複数の型パラメータと型の制約(Extends)を見てみよう。
/
- オブジェクトから特定のプロパティだけを安全に抽出する関数
- @template {Record
} T – Tは必ずオブジェクト型であるべきという制約 - @template {keyof T} K – KはTのプロパティキーのunion型に限定する
- @param {T} obj – 対象のオブジェクト
- @param {K} key – 抽出ししたいキー
- @returns {T[K]} 抽出されたプロパティの値の型
/
function getProperty(obj, key) {
return obj[key];
}
// — 使用例 —
const settings = {
theme: “dark”,
retries: 3,
isDebug: true
};
// 成功例:キーとして “theme” を渡すと、戻り値の型は “dark” などの文字列型として推論される
const currentTheme = getProperty(settings, “theme”);
// 失敗例(エディタが警告を出してくれる):
// 存在しないキーを指定すると、JSDoc/TypeScriptの型チェックがエラーを教えてくれる
// const invalid = getProperty(settings, “notExistKey”); // ⇐ ここで静的解析エラー!
ここで注目してほしいのは `@template {keyof T} K` という記述だ。
「第2引数に渡せるのは、第1引数のオブジェクトに存在するキーだけにしなさい」という強烈な制約をJSDocだけで表現している。これによって、タイポによるバグをランタイム(実行時)に到達する前に、エディタ上で完全に潰すことができる。
—
チーム開発で事故らないためのシニアからの忠告
JSDocによるジェネリクスは非常に強力だけど、現場で運用する上ではいくつか泥臭い注意点がある。
1. JavaScriptのランタイム動作には一切影響しない
JSDocはあくまで「コメント」だ。JavaScriptのエンジン(V8など)は実行時にJSDocを完全に無視する。つまり、JSの動的な挙動そのものは変わらない。型チェックは「開発時のエディタ(VS Codeなど)」や「CIでの型チェック(`tsc –checkJs`)」の仕事であることを忘れるな。
2. 複雑にしすぎない
TypeScriptの高度な型パズル(Conditional TypesやMapped Typesなど)をJSDocでやろうとすると、コメントの記述が本実装のコードより難解になり、チームメンバーのメンタルモデルを破壊する。
「複雑な型が必要になったら素直にTypeScript(`.ts`)への移行を検討する」という判断基準をチームで持っておくのが、シニアとしての健全な舵取りだ。
3. `// @ts-check` をファイルの先頭に置くのを忘れるな
これが一番やりがちだ。JSDocを書いても、ファイルの先頭に `// @ts-check` を記述していなければ、VS Codeはただのコメントとしてスルーしてしまう。型チェックを有効化したいファイルの先頭には必ずこの魔術の呪文を仕込んでおこう。
—
まとめ
JSDocの `@template` を使いこなせるようになると、Vanilla JSの軽快さを維持したまま、モダンな型安全の恩恵をフルに受けることができる。
レガシーなスクリプト資産を抱えているプロジェクトや、あえてビルドステップを複雑にしたくないライブラリ開発において、これは間違いなく現場の戦闘力を底上げする強力な武器になる。
まずは手元の開発環境の `.js` ファイルの先頭に `// @ts-check` を書き、小さな関数から `@template` を導入してみてほしい。エディタが返す補完の精度が変わった瞬間、きっと面白さが分かるはずだ。
さて、手を動かす時間だ。頼んだぞ!

コメント