アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

ESDoc を試す 2 - 独自型の定義

November 16, 2015開発ES6, ESDoc, JavaScript

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 の @propertyFraction を指定している。このように独自型を定義してゆくとそれらをプリミティブ型が登場するまで詳細に定義したくなり、そうした型は一箇所に集めておいたほうが便利である。

ESDoc 上の表示

@typedef を持つプロジェクトに対して ESDoc を走らせてみた。ESDoc のインストールや実行方法については ESDoc を試す を参照のこと。出力結果 HTML を開くと以下のように解析されている。

独自の型

MusicMetadata という型のプロパティで、同じく @typedef により定義された FractionPicture がリンク化されていることがわかる。もちろん、これらをクリックすると個々の型情報を表示できる。

@typedef で定義した型を引数として持つ関数や getter の戻り値などに指定した場合も型として解析されることが確認できた。

引数と 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 はプリミティブ型の名前について大文字・小文字を無視する (numberNumber はどちらでもよい) のだがショートコードで functionFunction にすると正しく解析されず内部の型がリンク化されないので注意すること。

私が Sublime Text で愛用している spadgos/sublime-jsdocs が自動生成する JSDoc コメントは型名が PascalCase になる。そのため大文字で開始する習慣がついているので、対応可能であればショートコードの functionFunction で解析されるようになるとありがたい。

@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 の提供する EventEmitterBuffer の解説。それぞれ公式ドキュメントの冒頭文を文章として引用、@see にて元ページの URL を明示。ESDoc による解析も効く。

コメントと @see

外部フレームワークやライブラリの型も定義する場 @typedef もその単位で分割したほうが扱いやすい。私はプロジェクトの JavaScript ディレクトリ配下に typedef というディレクトリを設けて App.jsNode.jsElectron.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 で解析すると以下のようになる。

@external で定義された型

他の型と異なり表記が 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 を利用するようにしたい。