ドキュメントが嘘をつく前に:JSDoc→TypeDoc自動生成とREADME・ADRの保守
コメントとコードがズレた瞬間ドキュメントは嘘になる。JSDoc/TSDocからTypeDocでリファレンスを生成し、READMEとADRを陳腐化させない仕組みを、Claude Codeと検証スクリプトで回す手順にまとめました。
関数のコメントに「ユーザーIDを返す」と書いてあったので、僕はそれを信じてコードを書きました。実際の戻り値は、ユーザーIDではなくユーザーオブジェクトでした。
コメントが書かれたのは半年前。その後で戻り値の型が変わり、誰もコメントを直さなかった。ドキュメントは、書いた瞬間から少しずつ嘘になっていきます。
これが厄介なのは、間違ったドキュメントは「ない」より悪いことです。何もなければコードを読む。でも自信たっぷりに間違ったことが書いてあると、人はそれを信じて時間を溶かします。
だから僕はあるときから、ドキュメントを「手で書いて頑張って保守する」のをやめました。代わりに、コードから生成できるものは生成し、生成できないもの(なぜ・README)だけ手で書き、両方を機械で見張る。今日はその仕組みを、コピペで動く形で渡します。
この記事の要点
- ドキュメントは3層に分ける。①コードから生成できる「APIリファレンス」(JSDoc/TSDoc→TypeDoc)、②手で書く「README」、③設計の理由を残す「ADR」。
- 関数の説明はコメントに書いてTypeDocで生成する。手書きのリファレンスは必ず腐るので作らない。
- READMEとADRはClaude Codeに下書きさせ、「証拠がない項目は書かない」と縛る。AIは存在しないコマンドを自然に書く。
- 公開前に検証スクリプトを1本通す。リンク切れ・必須見出し欠落・未確定プレースホルダを機械で止める。
- 最初からhooksで全自動にしない。手でコマンドを回し、何を止めたいか分かってから自動化する。
ドキュメントを「層」で考えると保守がラクになる
ドキュメントをひとくくりにするから保守がつらくなります。性質がまったく違うものが混ざっているからです。僕はいつも3つに分けます。
| 層 | 例 | 誰が書く | 腐りやすさ |
|---|---|---|---|
| APIリファレンス | 関数・型・引数の説明 | コードから自動生成 | 生成すれば腐らない |
| 使い方(README) | セットアップ、起動、つまずき | 人間(下書きはAI) | 中(コマンドが変わると腐る) |
| 設計の理由(ADR) | なぜこの方式を選んだか | 人間 | 低(過去の判断は変わらない) |
ポイントは、APIリファレンスを手で書かないことです。「この関数はユーザーIDを返す」みたいな説明を手書きのドキュメントに置くと、冒頭の僕のように必ずズレます。これはコメントに書いて、コードから生成する。そうすれば型が変われば生成物も変わる。嘘が入る隙間が消えます。
逆に「なぜこの設計にしたか」は、コードのどこを読んでも出てきません。これは人間が書くしかない。だからADR(後述)として別に残す。層を分けると、どこに何を書くか、どこを自動化するかが一気にはっきりします。
まず動かす:JSDoc/TSDocからTypeDocでリファレンスを生成する
説明より動かしたほうが早いです。TypeScriptのコメント(JSDoc/TSDoc記法)から、HTMLのAPIリファレンスを生成します。TypeDocは公式サイトが「TypeScriptのソースコード中のコメントをHTMLドキュメントまたはJSONモデルに変換する」と説明しているとおりのツールです。Node.jsがあれば数分で動きます。
まず準備します。
mkdir doc-demo && cd doc-demo
npm init -y
npm install --save-dev typescript typedoc
mkdir src
次に、コメント付きのソースを1つ書きます。/** ... */ で始まるのがTSDoc/JSDocのコメントです。@param(引数の説明)や@returns(戻り値の説明)、@example(使用例)といったタグを付けると、生成されるリファレンスに反映されます。src/tasks.ts として保存してください。
/**
* タスク1件を表す。status は3状態のいずれか。
*/
export interface Task {
/** 一意なID(UUIDなど) */
id: string;
/** 表示用のタイトル */
title: string;
/** 進行状況 */
status: "todo" | "doing" | "done";
}
/**
* タイトルから新しいタスクを作る。
*
* @param title - タスクのタイトル。空文字は弾く。
* @returns status が "todo" の新しい {@link Task}
* @throws title が空のとき Error を投げる
*
* @example
* ```ts
* const t = createTask("買い物");
* // => { id: "...", title: "買い物", status: "todo" }
* ```
*/
export function createTask(title: string): Task {
if (title.trim() === "") {
throw new Error("title は空にできません");
}
return {
id: crypto.randomUUID(),
title,
status: "todo",
};
}
そして生成します。--out docs/api で出力先を指定し、src/tasks.ts をエントリーポイントにします。
npx typedoc src/tasks.ts --out docs/api
docs/api/index.html をブラウザで開くと、createTask の引数・戻り値・使用例、Task の各プロパティの説明が、コメントから組み上がったページとして出てきます。コメントを直して再生成すれば、ドキュメントも必ず追従する。 これが「手書きしない」効果です。
TSDocは公式によれば「TypeScriptのドキュメントコメントを標準化する提案」で、TypeDoc・API Extractor・ESLint・VS Codeなどが各自の方言でコメントを解釈してしまう問題を揃えるためのものです。難しく考えず、最初は@param・@returns・@exampleの3つだけ使えば十分です。VS Codeのホバー表示にも同じコメントが出るので、書いた説明が二重に効きます。
READMEは「表紙」ではなく「最初の30分」の手順書
APIリファレンスを自動化したら、人間が書くのはREADMEとADRだけになります。READMEで僕が意識しているのは、読み手を「さっき初めてcloneした人」に固定することです。プロジェクトの理念より、いま手元で打つコマンドを先に書く。
最低限、この4つを順番に置けば迷子は減ります。
- セットアップ(インストールから起動まで、コピペで打つコマンド)
- よく使うコマンド(表にする。説明文より一覧が速い)
- つまずいたときの戻り方(失敗例と回復手順)
- 次に読むもの(API リファレンスや設計メモへのリンク)
Claude Codeに下書きさせる場合、放っておくと抽象的な説明文が増えます。「使いやすくして」ではなく、「初回セットアップで打つコマンド、失敗時の戻り方、次に読むリンクを必ず入れて。package.jsonのscriptsにないコマンドは書かないで」と縛ると、実用的なものが出ます。コマンドの存在確認まで指示するのがコツです。
生成後のREADMEは、たとえばこういう骨格になります。コマンドは表にして、つまずきポイントを先回りで書いておく。
# Task API
## Getting started
```bash
npm ci
npm run dev
```
## Commands
| Command | 用途 |
| --- | --- |
| `npm run dev` | ローカルサーバを起動 |
| `npm run test` | ユニットテスト |
| `npm run docs` | APIリファレンスを生成 |
| `npm run docs:verify` | ドキュメントを検証 |
## Troubleshooting
- インストールに失敗したら `node_modules` を消して `npm ci` をやり直す
- APIの例が動かないときは、サーバが記載のポートで起動しているか確認する
- 知らない環境変数が出てきたら `.env` ではなく `.env.example` を見る
CLAUDE.mdに「READMEはこの4項目を守る」と書いておくと、Claude Codeが毎回その型で下書きしてくれます。何を書かせ、何を書かせないかの粒度はCLAUDE.mdは「何を書かないか」で決まるで詳しく扱っています。新人の立ち上げ全体をREADMEから設計する話はClaude Code前提のオンボーディング設計が近いです。
ADRで「なぜ」を残す:コードに書けない唯一の情報
コードを読めば「何をしているか」は分かります。でも「なぜそうしたか」は、どれだけ読んでも出てきません。「PostgreSQLじゃなくSQLiteにしたのはなぜ?」「この再試行回数が3回なのはなぜ?」——これを残すのがADR(Architecture Decision Record、設計判断の記録)です。
ADRは長い設計書ではありません。1つの判断を、状況・決定・影響の3点で短く残すメモです。Claude Codeは差分から「何を変えたか」は拾えますが、理由は人間が確認しないと推測で埋まります。下書きを作らせて、理由だけ自分で直すのが現実的です。
# ADR-0001: APIリファレンスはTypeDocで生成し手書きしない
## Status
Accepted
## Context
関数の戻り値や引数の説明を手書きdocsに置くと、コード変更時に更新が漏れ、
ドキュメントが実装と食い違う。実際に「ユーザーIDを返す」と書かれた関数が
オブジェクトを返しており、読者が時間を溶かした事故が起きた。
## Decision
APIリファレンスはJSDoc/TSDocコメントとしてコード内に書き、
TypeDocで docs/api/ にHTMLを生成する。手書きのAPI一覧は作らない。
READMEとADRだけを手書きで保守する。
## Consequences
- 型や引数が変わればリファレンスも追従し、説明のズレが起きにくい
- レビュアーは「コメントが正しいか」だけ見ればよい
- TypeDocの生成物 docs/api/ はビルド成果物として扱い、検証対象から外す
避けたいのは、ADRを作業ログにすることです。「READMEを更新した」はADRではありません。「APIリファレンスを手書きしないと決めた」という判断と、その理由・影響があるから後から効きます。1ファイル1判断、連番で積んでいくだけでいいです。
ドキュメントの陳腐化を防ぐ:公開前に止める検証スクリプト
ここが本題です。生成しても下書きしても、公開前に機械で見張らないとドキュメントは腐ります。人間の目だけに頼ると、忙しい日に必ず素通りします。
次のスクリプトは、(1)必須ファイルと必須見出しの存在、(2)README内のリンク切れ(ローカルファイルへの相対リンク)、(3)TODO/TBD/REPLACE_MEなどの未確定プレースホルダ、(4)Markdownコードフェンスの数が偶数かを確認します。scripts/verify-docs.mjs として保存し、node scripts/verify-docs.mjs で実行してください。
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
// 必須ファイルと、その中に必ずあってほしい見出し
const required = [
{ file: "README.md", phrases: ["## Getting started", "## Commands"] },
{ file: "docs/adr/0001-typedoc-reference.md", phrases: ["## Status", "## Decision"] },
];
const errors = [];
function read(file) {
return readFileSync(file, "utf8");
}
// 1. 必須ファイル・必須見出しのチェック
for (const item of required) {
if (!existsSync(item.file)) {
errors.push(`${item.file}: ファイルがありません`);
continue;
}
const text = read(item.file);
for (const phrase of item.phrases) {
if (!text.includes(phrase)) {
errors.push(`${item.file}: 見出し "${phrase}" がありません`);
}
}
}
// 2. READMEのローカルリンク切れチェック(http(s)は対象外)
if (existsSync("README.md")) {
const readme = read("README.md");
const linkPattern = /\]\((?!https?:|#)([^)]+)\)/g;
let match;
while ((match = linkPattern.exec(readme)) !== null) {
const target = match[1].split("#")[0];
if (target && !existsSync(path.resolve(target))) {
errors.push(`README.md: リンク切れ -> ${target}`);
}
}
}
// 3 & 4. プレースホルダ残りと、コードフェンスの偶奇チェック
const fence = String.fromCharCode(96, 96, 96);
for (const file of ["README.md", ...required.map((r) => r.file)]) {
if (!existsSync(file)) continue;
const text = read(file);
if (/TODO|TBD|REPLACE_ME/.test(text)) {
errors.push(`${file}: 未確定プレースホルダが残っています`);
}
const fenceCount = (text.match(new RegExp(fence, "g")) ?? []).length;
if (fenceCount % 2 !== 0) {
errors.push(`${file}: コードフェンスの数が奇数です(閉じ忘れの可能性)`);
}
}
if (errors.length > 0) {
console.error("ドキュメント検証に失敗しました:");
for (const e of errors) console.error(`- ${e}`);
process.exit(1);
}
console.log("ドキュメント検証OK");
これだけで完璧にはなりません。でも「READMEが消えた」「ADRにStatusがない」「リンク先のファイルが消えてリンク切れ」「TODOを埋め忘れて公開」みたいな、地味で恥ずかしい事故は止まります。
package.json にコマンドを登録しておくと、Claude Codeにも人間にも同じ確認を渡せます。
{
"scripts": {
"docs": "typedoc src/tasks.ts --out docs/api",
"docs:verify": "node scripts/verify-docs.mjs"
}
}
CIにnpm run docs:verifyを1行足せば、リンク切れや見出し欠落のままマージされるのを止められます。さらに強く自動化したいなら、Claude Codeのhooksでファイル編集後に検証を走らせる手もあります。ただし最初からhooksを入れるより、まず手でdocs:verifyを回し、何を止めたいか分かってから設定するほうが失敗しません。検証を成果物として残す考え方はClaude Code検証レシート運用が参考になります。
僕がやらかしたドキュメントの失敗3つ
正直に書きます。最初は全部やらかしました。
ひとつ目は、APIリファレンスを手で書いたこと。冒頭の「ユーザーIDを返す」事故がこれです。関数の説明をWiki に手書きしていた頃は、コードを直すたびにWikiも直す必要があって、当然それを忘れる。TypeDocに切り替えてから、この種のズレは消えました。説明をコードの隣(コメント)に置くだけで、更新漏れの構造そのものがなくなります。
ふたつ目は、Claude Codeにドキュメントを「いい感じに」頼んだこと。存在しないnpm run buildを堂々とREADMEに書かれて、新人がハマりました。AIは「ありそうなコマンド」を自然な文章で書きます。package.jsonのscriptsを見せて「ここにないコマンドは書くな」と縛ってから、嘘コマンドが激減しました。Claude Codeに何を許すかは権限設定リファレンスで型にできます。
みっつ目は、検証を人間の目だけに頼ったこと。「公開前に僕が読めばいい」は、締め切り前の夜に必ず破綻します。リンク切れと未確定プレースホルダを機械で弾くようにしてからは、恥ずかしい公開ミスがほぼゼロになりました。機械でわかることは、機械に見張らせる。これに尽きます。
よくある質問
Q. JSDocとTSDoc、TypeDocの違いは? A. JSDocはコメントの古くからある記法、TSDocはそれをTypeScript向けに標準化する提案、TypeDocはコメントからリファレンスを生成するツールです。実務では「TSDoc記法でコメントを書き、TypeDocで生成する」と覚えれば十分です。
Q. JavaScriptプロジェクトでもTypeDocは使える?
A. TypeDocはTypeScript前提です。素のJSならjsdocコマンドやTypeDocの--js系オプションを検討するか、// @ts-checkとJSDoc型注釈を併用してTypeScript扱いにする手があります。新規ならTypeScript化したほうが後がラクです。
Q. ドキュメントの英語と日本語、どちらで書く? A. APIリファレンスのコメントは英語が無難です(記号やタグと混ざっても崩れにくい)。READMEとADRは読み手の言語で。社内向けなら日本語で構いません。大事なのは言語より、説明とコードがズレていないことです。
Q. Claude Codeが生成したドキュメントはそのまま公開していい?
A. だめです。AIは存在しないコマンドやエンドポイントを自然な文章で書きます。最低でもdocs:verifyを通し、コマンドとリンクは実際に動くか確認してから公開してください。
Q. 小さい個人プロジェクトでもADRは必要?
A. 1人でも「半年後の自分」が読者です。「なぜSQLiteにしたか」を3行残すだけで、後から自分を救えます。最初はdocs/adr/に1ファイル置くところからで十分です。
実際に試した結果
この記事のために、createTaskのような関数にTSDocコメントを付けてTypeDocで生成し、READMEとADRはClaude Codeに「scriptsにないコマンドは書くな」と縛って下書きさせ、最後にverify-docs.mjsを通す流れを実際に回しました。
効いたのは、手書きのAPIリファレンスを完全にやめたことです。コメントを直せば生成物が追従するので、「ドキュメント直し忘れ」という作業自体が消えました。一方で、READMEのコマンドとADRの「なぜ」は、AIに任せきりだと推測が混じる。ここは下書きまでAI、確認は人間、という分担に落ち着きました。
賢くドキュメントを書かせることより、腐らない置き場所(コメント)に置いて、機械で見張る仕組みを先に作る。遠回りに見えて、これがいちばん嘘の少ないドキュメントになる、というのが今の実感です。
毎回同じ指示を書くのが面倒になったら、プロジェクトごとのルールをCLAUDE.mdテンプレ集で型にしておくと、ドキュメント生成の指示を毎回考えずに済みます。チーム導入でREADME・リファレンス・ADRの運用まで一緒に整えたいときは、導入相談から現場に合わせて設計するのが早いです。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Codeに1ファイルだけ直させる指示文のつくり方
「もっと良くして」で40行も変えられた失敗から学んだ、触る範囲・検証・戻し方をセットにしたClaude Code用の依頼文テンプレートを紹介します。
Claude Code の権限拒否から復旧する: 止まった理由を次の安全手順に変える
Claude Code のコマンドが拒否されたとき、焦って許可を広げずに、拒否理由、代替手順、証拠コマンド、再試行条件へ分解する方法。
Claude Codeにビルド→スモークテスト→自動修正を回させる足場の作り方
最小スモークテストの選び方、失敗ログを食わせて直させるループ、回数上限と確認ゲートで暴走を止める方法を、コピペで動くコード付きで紹介します。