【実務・中級編】 @typedefによるカスタム型定義 – JavaScript実践ガイド

やあ。今日も今日とてコードレビューに追われてるかい?
「動くには動くけど、このオブジェクトの中に一体何のプロパティが入っているんだっけ?」――そんなカオスなコードに頭を抱えた経験、君にもあるはずだ。

TypeScriptがフロントエンドのデファクトスタンダードになりつつある昨今だけど、様々な事情やレガシーとの兼ね合い、あるいはビルドステップを極限まで削ぎ落とした軽量スクリプトとして、「あえて純粋なJavaScript(Vanilla JS)で書きたい」という現場もまだまだ多い。

そんな時、JSDocの `@typedef` を使いこなせているかどうかで、君の書くJavaScriptの信頼性は天と地ほどの差が出る。今日は、型のない世界に秩序をもたらす `@typedef` によるカスタム型定義の極意を、現場のリアルな視点から伝授しよう。

—

なぜ、いま「@typedef」なのか?

JavaScriptは動的型付き言語だ。変数には何でも入れられるし、オブジェクトには後から自由気ままにプロパティを生やせる。これがプロトタイピングのスピードを爆上げする一方で、中規模・大規模開発においては最大の爆弾になる。

「TypeScriptを使えばいいじゃん」って? まあ落ち着きなよ。
TypeScriptの導入コストを払えない環境、あるいはJSDocコメントだけでエディタ(VSCodeなど)の強力なIntelliSense(入力補完)や静的解析(Type Checking)をフル活用したい場面は実務でゴロゴロ転がっている。

ブラウザは実行時にJSDocなんてただの「コメント」として華麗にスルーする。つまり、ランタイムのパフォーマンスを一切落とすことなく、開発時のみTypeScript並みの堅牢性を手に入れられる――これこそが、 `@typedef` をマスターする最大のメリットなのさ。

—

基本の構文とエディタの裏側の動き

まずは基本を押さえよう。
`@typedef` は、JSDocの `@type` と組み合わせて、独自のオブジェクト構造や複合型に名前を付けるためのものだ。

VSCodeなどのモダンなエディタは、裏側でTypeScriptの言語サーバー(tsserver)を走らせている。つまり、JSDocで書かれた型定義は、エディタ上では立派なTypeScriptのインターフェースとして解釈されているんだ。

百聞は一見に如かず。実務でよくある「ユーザー情報」のオブジェクトを定義してみよう。

/

  • ユーザーの権限レベルを表す型
  • @typedef {‘admin’ | ‘editor’ | ‘viewer’} UserRole

/

/

  • アプリケーション内で使用するユーザー情報のカスタム型
  • @typedef {Object} User
  • @property {string} id – ユーザーを一意に識別するUUID
  • @property {string} name – ユーザーの表示名
  • @property {string} email – 連絡先のメールアドレス
  • @property {UserRole} role – ユーザーの権限
  • @property {string} [avatarUrl] – プロフィール画像のURL(オプショナル)

/

ポイントは、プロパティ名のまわりに `[]` をつけることでオプショナル(省略可能)を表現できる点だ。TypeScriptの `avatarUrl?: string` と全く同じ意味になる。

—

現場で即戦力になる!複雑なオブジェクトの型定義

実務のコードはもっと泥臭い。APIから返ってくるレスポンスデータはネストしているし、配列やコールバック関数だって含まれる。
ここでは、現場でそのままコピペして使える、少し複雑で実践的なサンプルを見せよう。

/

  • @file user-service.js
  • @description ユーザー管理サービス(JSDocによる型安全なVanilla JS実装)

/

/

  • サーバーから返却されるAPIのエラー情報
  • @typedef {Object} ApiError
  • @property {number} code – HTTPステータスコード
  • @property {string} message – エラーメッセージ
  • @property {Object.} [details] – バリデーションエラーの詳細など(キー・バリューが共に文字列の辞書型)

/

/

  • ページネーション情報
  • @typedef {Object} PageInfo
  • @property {number} currentPage – 現在のページ番号
  • @property {number} totalPages – 総ページ数
  • @property {boolean} hasNext – 次のページが存在するかどうか

/

/

  • ユーザー一覧のAPIレスポンス全体
  • @typedef {Object} UserListResponse
  • @property {import(‘./types’).User[]} users – ユーザーオブジェクトの配列
  • @property {PageInfo} pagination – ページネーションメタデータ

/

class UserService {
constructor() {
/ @private @type {string} /
this.baseUrl = ‘https://api.example.com/v1’;
}

/

  • 指定したページ番号のユーザー一覧を取得する
  • @param {number} [page=1] – 取得するページ番号
  • @returns {Promise} ユーザー一覧とページネーション情報を含むPromise
  • @throws {ApiError} 通信失敗時やAPIエラー時のカスタムエラー

/
async fetchUsers(page = 1) {
try {
const response = await fetch(`${this.baseUrl}/users?page=${page}`);

if (!response.ok) {
/ @type {ApiError} /
const errorData = await response.json();
throw errorData;
}

/ @type {UserListResponse} /
const data = await response.json();
return data;

} catch (error) {
// 現場の罠:catch(error)のerrorはデフォルトで `unknown` (anyに近い扱い) になるため、
// 型キャストしてあげることで後続の処理でプロパティ補完が効くようになる
const apiError = / @type {ApiError} / (error);
console.error(`API Error [${apiError.code}]: ${apiError.message}`);
throw apiError;
}
}
}

このコードの何が素晴らしいって、`fetchUsers` の戻り値やエラーの構造が完全にドキュメント化されている点だ。他の開発者がこのメソッドを呼び出す時、VSCodeの補完によって `users` の中身や `pagination` のプロパティがサクサクとサジェストされる。コード自体がドキュメントになる瞬間だな。

—

型定義ファイルを別名で分離してスッキリさせる運用術

さて、ここで一つの疑問が湧くはずだ。「こんな型定義を毎回すべてのJSファイルの先頭に書いてたら、コードがコメントだらけになって読みにくくないか?」と。

その通り。だからこそ、実務では型定義を集約したファイル(`.js` または `.d.ts`)を分離するのがプロの常道だ。

プロジェクトのルートや `src/types/` ディレクトリに `global.js` あるいは `types.js` というファイルを作り、そこに `@typedef` を集約しよう。

// src/types.js

/

  • @typedef {Object} User
  • @property {string} id
  • @property {string} name
  • @property {‘admin’ | ‘editor’ | ‘viewer’} role

/

// ESモジュールとして他のファイルから読み込めるように空のエクスポートを書いておく
export {};

そして、別のファイルでこの型を使いたい時は、JSDocの `@typedef` や `@type` の中でパスを指定してインポートすればいい。

// src/app.js

/

  • @typedef {import(‘./types.js’).User} User

/

/

  • ユーザーの権限をチェックする関数
  • @param {User} user – チェック対象のユーザー
  • @returns {boolean} 管理者権限を持っているかどうか

/
function isAdmin(user) {
return user.role === ‘admin’;
}

これで、TypeScriptを使わずとも、完全にモジュール化されたクリーンな型エコシステムがJavaScriptの中に構築できるわけだ。

—

シニアからの実践アドバイス

最後に、現場で `@typedef` を導入する際によやりがちなアンチパターンと、それを避けるための心構えをいくつか伝えておこう。

1. 「any」への逃げ込みに注意する
複雑な型を定義するのが面倒だからといって、何でもかんでも `@type {any}` や `@type {Object}` と書くのは、型安全性をドブに捨てるようなものだ。最初は面倒でも、主要なデータ構造だけでも `@typedef` で型を縛っておくことで、後々のリファクタリング地獄を防げる。

2. JSDocの記述ミスによるサイレントエラー
TypeScriptと違って、JSDocの型記述のタイポ(例: `@property {string} id` と書くべきところを `@property {strign} id` と打つなど)は、静的解析ツールが気づきにくい場合がある。VSCodeのホバープレビュー等で、ちゃんと意図した型として認識されているかこまめに確認する癖をつけよう。

3. チーム全員の共通認識を作る
「JavaScriptなのに、なんでこんなにコメント書くの?」というチームメンバーが出てくるかもしれない。そんな時は、「これはドキュメントであり、エディタの補完を爆上げしてバグを未遂で防ぐための開発支援ツールなんだよ」と優しく諭してあげてほしい。

TypeScriptへの移行コストを払うまでもない小〜中規模なプロジェクト、あるいはパフォーマンスと軽量さを最優先したいVanilla JS環境において、`@typedef` は最強の武器になる。

さあ、明日からのコードで、コメントアウトの海に美しい型のエッセンスを散りばめてみないか? 君の書くコードの洗練度を、チームの全員がきっと見直すはずさ。

コメント

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