JSDoc 書いていますか?VSCode とかでホバーすると定義が出てくるのが嬉しいですよね。
TypeScript で書くと TSDoc とも言われますが、この記事では JavaScript TypeScript 両方とも JSDoc と表記します。
世間にたくさんの JSDoc の記事があるので、ここでは私が便利だなと思った「こんなところに書いてもいいんだ!」と思った JSDoc について解説します。
前段:コメント書くかどうか議論
書くべきコメント、または書くべきではないコメント。これは語り尽くされた話かなと思います。
5W1H の大体は Git (または他の VCS)に付随する情報として表現されている、コード中の変数や関数等の命名がそれ自体を説明している……。書くべきなのは "Why Not"1 である、など…。
関数名が的を射ていれば JSDoc を書く必要がないとする方もいるかと思いますが、読み手(それは未来の自分も)に素早く意図が伝わるメリットは大きいと感じます。
実装が変更された場合にコメントが変更されないという問題についての回答は、「コメントも変更しましょう」なのかなと思います。
JSDoc とは
JSDoc の公式サイトは Use JSDoc: Index のようですが、 JSDoc タグをたどってみるといいかも。JSDoc タグが無い、JSDoc について解説したページも結構あるようなので、JSDoc で検索も併用すると良さそうです。
変数にコメントを付ける簡単な例。
/**
* 初期選択された値。
*/
const defaultValue = "all"
こう書くことで、離れたところでこの変数を参照する場合、VSCode などのエディタ上で参照箇所の変数にカーソルをホバーすると、ツールチップが出て JSDoc の内容が出てくれるのが嬉しい。
( スクリーンショットだと隣接しているのでありがたみが皆無ですが)
GAS でも使えるのが嬉しいです。ただ、GAS の場合は変数等のタイピングの際の補完時にのみ表示され、詳細はカーソルで最初のツールチップクリックでのみ表示、カーソルホバーで出すことはできないようです2。
単一の変数に書いても嬉しさがありますが、やはりありがたいのは一定の構造を持ったもの。例えば関数には @param
で引数、 @return
で返り値について言及できます。波括弧 {}
で変数型に言及することもできます。
/**
* 2つの値を足した値を返す。
* @param {number} a
* @param {number} b
* @returns {number} 2つの値を足した値
*/
function add(a, b) {
return a + b;
}
なお、TypeScript の場合は、型定義が文法中に行えるので、わざわざ JSDoc に書く必要はないでしょう3。
/**
* 2つの値を足した値を返す。
* @param a
* @param b
* @returns 2つの値を足した値
*/
function add(a: number, b: number): number {
return a + b;
}
GAS の標準エディタでは TypeScript は現状使えないわけで、JSDoc 中の型への言及も知っておくと役立ちます。なお JSDoc における型への言及で使える型名は、TypeScript の型が概ね使えるようです4。
なお、JSDoc は /**
から始まって */
で終わる形式ですが、1行に収めることもできます。複数行の場合は、縦に *
が並ぶように書くのですが、1行バージョンではそれがない感じ。
/** セレクトボックスで初期選択されている年齢 */
const defaultAge = 25;
関数のように複数の要素を説明する場合は複数行バージョンがいいですが、単純な値の解説は1行バージョンがコンパクトでいいかもしれませんね。
こんなところにも書いて嬉しい JSDoc
ここからは、オムニバス形式で、筆者が「こんなところにも書けるんだ!嬉しい!」と思った JSDoc をご紹介。
TypeScript のオブジェクト型定義のプロパティ部分
TypeScript では日常的に型定義を行います。
例えば一塊の睡眠を表す SleepSection 型を書いてみます。
type SleepSection = {
start: Date;
end: Date;
type: "就寝" | "二度寝" | "うたた寝" | "その他";
description: string;
}
これに型をつける場合、まず SleepSection 自体に型を付けますが、実は、その中のプロパティ部分にも個別に JSDoc を書くことができます。
/**
* 就寝から起床の睡眠時間を表す。
*/
type SleepSection = {
/** 睡眠開始時間 */
start: Date;
/** 睡眠終了時間 */
end: Date;
/** 睡眠時間の種類 */
type: "就寝" | "二度寝" | "うたた寝" | "その他";
/** 睡眠時間の説明 */
description: string;
}
通常はオブジェクト型名やそのプロパティ名は読んでわかるように努めるでしょうが、アプリの規模が大きくなってデータが多数かつ複雑になるほど、名前だけですべてを表すことが難しくなります。そういうときに、このテクニックが役立ちます。
なお、タプルはこのようなことができないようです
/**
* 経度と緯度のペアを表す。
*/
type LngLatPair = [number, number];
どっちが経度(Longitude = Lng) で、どっちが緯度 (Latitude = Lat)?
// 以下のようにしても、軽度と緯度のところの JSDoc を型参照先で知ることはできない!
/**
* 経度と緯度のペアを表す。
*/
type LngLatPair = [
/** 経度 */
number,
/** 緯度 */
number
];
こういうときは、型定義に実際は不要でも、仮変数的なものを書くと良いようです。
/**
* 経度と緯度のペアを表す。
*/
type LngLatPair = [
lng: number,
lat: number
];
型定義に不要であれば、日本語で書いてもいいのではないか?バージョン5。
/**
* 経度と緯度のペアを表す。
*/
type LngLatPair = [
経度: number,
緯度: number
];
クラスのプロパティ
先ほどは、プレーンなキーバリュー型構造のオブジェクトに対して型定義をしたものですが、場合によっては class を定義することもあるでしょう。
/**
* ユーザークラス
* @class
*/
class User {
/**
* ユーザーを作成します
* @param {string} name - ユーザーの名前
* @param {number} age - ユーザーの年齢
* @param {Object} options - 追加設定
* @param {string} [options.email] - メールアドレス(オプション)
* @param {boolean} [options.isAdmin=false] - 管理者権限の有無
*/
constructor(name, age, options = {}) {
this.name = name;
this.age = age;
this.email = options.email;
this.isAdmin = options.isAdmin ?? false;
}
/**
* ユーザー情報を文字列として返します
* @returns {string} ユーザー情報の文字列表現
*/
toString() {
return `${this.name} (${this.age}歳)`;
}
}
しっかりした定義に思えますが、実際にプロパティの値参照部分(例えば toString()
定義中の this.name
の name
部分)にカーソルをホバーしても、「ユーザーの名前」といった解説は出てきません。 constructor
の上に書いた JSDoc は、あくまでも constructor の引数に対する言及でしかないためです。
ちゃんと対応するには、constructor 中の this.name
の代入部分に JSDoc を書いてあげると良いようです。
/**
* ユーザークラス
* @class
*/
class User {
/**
* ユーザーを作成します
* @param {string} name - ユーザーの名前
* @param {number} age - ユーザーの年齢
* @param {Object} options - 追加設定
* @param {string} [options.email] - メールアドレス(オプション)
* @param {boolean} [options.isAdmin=false] - 管理者権限の有無
*/
constructor(name, age, options = {}) {
// ↓ 新たに this.PROPNAME の代入箇所に JSDoc を書いた
/** ユーザーの名前 */
this.name = name;
/** ユーザーの年齢 */
this.age = age;
/** ユーザーのメールアドレス */
this.email = options.email;
/** ユーザーが管理者かどうか */
this.isAdmin = options.isAdmin ?? false;
}
/**
* ユーザー情報を文字列として返します
* @returns {string} ユーザー情報の文字列表現
*/
toString() {
return `${this.name} (${this.age}歳)`;
}
}
TypeScript の場合は、class 内の private 定義等で行います。
/**
* ユーザークラス
*/
class User {
/** ユーザーの名前 */
private name: string;
/** ユーザーの年齢 */
private age: number;
/** ユーザーのメールアドレス */
private email?: string;
/** ユーザーが管理者かどうか */
private isAdmin: boolean;
/**
* ユーザーを作成します
* @param name - ユーザーの名前
* @param age - ユーザーの年齢
* @param options - 追加設定
*/
constructor(
name: string,
age: number,
options: {
email?: string;
isAdmin?: boolean;
} = {}
) {
this.name = name;
this.age = age;
this.email = options.email;
this.isAdmin = options.isAdmin ?? false;
}
/**
* ユーザー情報を文字列として返します
* @returns ユーザー情報の文字列表現
*/
toString(): string {
return `${this.name} (${this.age}歳)`;
}
}
他にも色々あった記憶があるのですが、思い出したら都度追記していこうと思います。
-
「通常ならよく知られた実装をするはずなのに、ここではなぜそれを避けたのか」というコードの読み手の心情を先回りするように書くことです。この "Why Not" は Git の情報やコード中の命名からは読み取れないものです。 ↩
-
細かい操作を伝えづらいので、簡単なサンプルで試してみるのがオススメです。なお、VSCode 等でのホバーにあたる操作は、コマンドパレットを F1 キーで出して、「[表示またはフォーカス] ホバー」を選ぶことで実現できます。 ↩
-
プログラミングにおいて不要な二度書きは避けたいわけで、その身でも書く必要は無いでしょう。なお拡張子 .ts において JSDoc 中に型を言及しつつ、実際に TypeScript としては型定義をしない場合は、VSCode 等のエディタでは JSDoc の型への言及が取り入れられることは無いようです。すなわち、JSDoc の定義によらず、型推論が働くか、any 型とされるかの模様。 ↩
-
https://jsdoc.app/ 等での TypeScript の言及は見当たらないのですが、その定義自体を見ていくと、概ね TypeScript の借用(?)だと理解して良さそうです。 ↩
-
常識的に通常のプロジェクトで行われることはないでしょうが、JavaScript の仕様としても Unicode 日本語文字列による変数名は許可されています。 ↩