ESDoc を試す 2 - 独自型の定義
ESDoc を利用してコード ドキュメントを定義する際、明確な型をもたない Object
の扱いに困っていた。このことについて Twitter でつぶやいていたら作者の @h13i32maru さんから以下の情報をいただいた。
これまで JavaScript 以外の言語でも xDoc 形式のコード ドキュメントを利用してきた。例えば Java、C++、Objective-C で JavaDoc 形式のコメントを定義し JavaDoc や Doxygen でドキュメント生成していたのだが、これらは基本的に静的型付けなので特別な cast でも使用しない限り、なんらかの型を定義・明示することになる。
つまりコードドキュメントにおける型も曖昧になることはない。しかし JavaScript などの動的型付け言語 (Protoype Base というべき?) では Object
を柔軟に拡張して Duck Typing することが多い。そうしたとき、拡張した箇所をドキュメントに残すにはどうしたらよいか、というのが悩みであった。
ドキュメントなしでも困らぬように、わかりやすい命名や参照範囲の局所化を試みるのもよいだろう。しかし意図や目的の記述には自然言語であるコメントのほうが向くはず。また JSON について解説する場合も Wiki などに別途資料を作成するのではなく JSON の構造そのものをコード ドキュメントとして定義し、それを扱うコードと一緒にメンテナンスしたい。
というわけで ESDoc における @typedef
機能を試してみた。
更新履歴
- 2015/11/18 「
@external
で外部ライブラリの型を定義する」を追記。 - 2015/11/17 「
@typedef
をより詳しく記述する」を追記。
@typedef
の書き方
ESDoc では typedef タグにより仮想的な型を定義できる。調べてみると ESDoc の元になった JSDoc にも存在する機能だった。
書き方は非常に簡単。以下は参考例。
/**
* @typedef {Object} Fraction
* @property {Number} no Numerator.
* @property {Number} of Denominator.
*/
/**
* @typedef {Object} Picture
* @property {String} format Format of an image file ( "jpg" or "png" ).
* @property {Buffer} data Image data.
*/
/**
* @typedef {Object} MusicMetadata
* @property {Array.<String>} artist Artist names.
* @property {String} album Album name.
* @property {Array.<String>} albumartist Artist names of the album.
* @property {String} title Title.
* @property {String} year Release year. MP4v2 is a "YYYY-MM-DD".
* @property {Fraction} track Track number of a disc.
* @property {Fraction} disk Disc number of a multiple discs.
* @property {Array.<String>} genre Genre names.
* @property {Picture} picture Image data.
* @property {Number} duration Duration ( Seconds ).
*/
/**
* @typedef {Object} GraphicEqualizerPreset
* @property {String} name Preset name.
* @property {Array.<Number>} gains Prest gains.
*/
@typedef
タグに JavaScript 上の基底型と型名を定義。基底型は Object
以外でもよい。例えば Number
を別の型として明示したいなら Number
にする。型名はそのまま ESDoc の解析対象になる。@param
や @typedef
上の @property
に型として指定できる。
@property
は型を構成するプロパティを定義。書式は @param
と同じく、型名、変数名、コメントの順に記述してゆく。
@typedef
の定義場所は ESDoc の解析対象となるスクリプトなら、どこでもよい。型と密接な関係にあるスクリプトにするか @typedef
自体をまとめたファイルを作成することになるだろう。
私は後者を採用した。プロジェクト内に Typedef.js
というファイルを用意し、そこに@typedef
を集約している。はじめは前者で定義していたのだが実際の型ではないものがコードに含まれるのは邪魔だし @typedef
間の入れ子を定義したいなら位置が近いとわかりやすい、というのがその理由。
前述の例だと MusicMetadata の @property
に Fraction
を指定している。このように独自型を定義してゆくとそれらをプリミティブ型が登場するまで詳細に定義したくなり、そうした型は一箇所に集めておいたほうが便利である。
ESDoc 上の表示
@typedef
を持つプロジェクトに対して ESDoc を走らせてみた。ESDoc のインストールや実行方法については ESDoc を試す を参照のこと。出力結果 HTML を開くと以下のように解析されている。
MusicMetadata という型のプロパティで、同じく @typedef
により定義された Fraction
や Picture
がリンク化されていることがわかる。もちろん、これらをクリックすると個々の型情報を表示できる。
@typedef
で定義した型を引数として持つ関数や getter の戻り値などに指定した場合も型として解析されることが確認できた。
コールバック関数
JSDoc は @callback というタグによりコールバック関数を型として定義できる。しかし現時点の ESDoc は対応していないようだ。JSDoc の書式で @callback
を定義してみたところ解析結果には出力されなかった。
代替は冒頭へ引用した @h13i32maru さんの Tweet で紹介されているショートコードになる。試しに以下のクラスを定義してみる。
/**
* Sample class.
*/
export default class Sample {
/**
* Read a metadata from the music file.
*
* @param {String} filePath Path of the music file.
* @param {function(err: Error, metadata: MusicMetadata): undefined} cb Callback function.
*/
read( filePath, cb ) {
}
}
これを ESDoc で解析するとコールバック関数のショートコードが解析されることを確認できた。
コールバック関数の引数型に @typedef
で定義した MusicMetadata
を使用しているが、それもリンク化されている。
なお ESDoc はプリミティブ型の名前について大文字・小文字を無視する (number
と Number
はどちらでもよい) のだがショートコードで function
を Function
にすると正しく解析されず内部の型がリンク化されないので注意すること。
私が Sublime Text で愛用している spadgos/sublime-jsdocs が自動生成する JSDoc コメントは型名が PascalCase になる。そのため大文字で開始する習慣がついているので、対応可能であればショートコードの function
も Function
で解析されるようになるとありがたい。
@typedef をより詳しく記述する
この記事を書いたあとに @typedef
された型へのコメントと @see
による参照追加を試したので追記。
@typedef
を利用するときプロパティだけでなく対象となる型自体の説明を記述したくなる。その場合は通常の関数ヘッダー コメントのようにコメント ブロックへ直に文章を書けばよい。
ESDoc は JavaScript 標準の型については Mozilla Developer Network の解説ページにリンクしてくれるのだが Node や Electron の型についてはそうならない。こうした外部フレームワークやライブラリの型については @typedef
と一緒に @see
で明示的にリンクを設定するとよい。
以下はその例。
/**
* Many objects in Node.js emit events.
*
* @see https://nodejs.org/api/events.html
*
* @typedef {Object} EventEmitter
*/
/**
* Pure JavaScript is Unicode friendly but not nice to binary data.
* When dealing with TCP streams or the file system, it's necessary to handle octet streams.
* Node.js has several strategies for manipulating, creating, and consuming octet streams.
*
* @see https://nodejs.org/api/buffer.html
*
* @typedef {Object} Buffer
*/
これらは Node の提供する EventEmitter
と Buffer
の解説。それぞれ公式ドキュメントの冒頭文を文章として引用、@see
にて元ページの URL を明示。ESDoc による解析も効く。
外部フレームワークやライブラリの型も定義する場 @typedef
もその単位で分割したほうが扱いやすい。私はプロジェクトの JavaScript ディレクトリ配下に typedef
というディレクトリを設けて App.js
、Node.js
、Electron.js
という感じでアプリ固有とプラットフォーム系を分けている。
@external で外部ライブラリの型を定義する
「@typedef
をより詳しく記述する」を書いた後 @h13i32maru さんより、外部ライブラリなら @external
という記法が便利なことを教えていただいた。
型の解説が Web 上にドキュメント化されているなら、その URL だけ指定すればよい。記法は単純で @see
のように @external
の後に URL を記述するだけ。
/**
* @external {EventEmitter} https://nodejs.org/api/events.html
*/
/**
* @external {Buffer} https://nodejs.org/api/buffer.html
*/
このタグで定義したものを ESDoc で解析すると以下のようになる。
他の型と異なり表記が E になっている。もちろん、この型を @param
などに指定するとタグに設定した URL へのリンクになる。これは便利だ。
まとめ
@typedef
とコールバック関数のショートコードを利用することで独自型もドキュメント化できるようになった。どれぐらい詳細に型定義するのか?という点について議論の余地はありそうだが、第三者と共有するコードであれば私としては可能な限り詳細にするつもり。
あと前回の記事でコメントの書式が不完全だと ESDoc でエラーになると書いたが最新版では修正されている。意図的にエラーが起きるようにして実行すると以下のように ESDoc は停止せずエラー箇所を解析したうえで内容を指摘してくれる。
error: could not process the following code.
js/common/Sample.js
5| /**
6| * Read a metadata from the music file.
7| *
8| * @param {String} filePath Path of the music file.
9| * @param {function(err: Error, metadata: MusicMetadata): undefined} cb Callback function.
10| * @param {String}
11| */
12| read( filePath, cb, test ) {
... 中略 ...
warning: signature mismatch: Sample#read js/common/Sample.js#6
5| /**
6| * Read a metadata from the music file.
7| *
8| * @param {String} filePath Path of the music file.
9| * @param {function(err: Error, metadata: MusicMetadata): undefined} cb Callback function.
10| * @param {String}
11| */
12| read( filePath, cb, test ) {
その他、型によってアイコンを C (ES6 class)、V (variable)、T (typedef された型) のように表示するなどの機能強化がおこなわれている。そのためなるべく最新の ESDoc を利用するようにしたい。