JSDocで型を書いた JavaScript は、TypeScript のコンパイラ(tsc)でそのまま型チェックできます。 ファイルの拡張子は .js のままで、/** @type {string} */ のようなコメントが型注釈の役目をします。ビルドの手順を増やさずに、打ち間違いや型の食い違いを事前に見つけられます。

この記事では、TypeScript が解釈する JSDoc タグを一覧表にまとめ、すべてのタグを .js ファイルに書いて tsc --checkJs --noEmit にかけた結果(TS番号つきのエラー全文)を載せます。TypeScript 7.0 で扱いが変わった @enum と @constructor の置き換え方、// @ts-check・checkJs・jsconfig.json の違い、.js から .d.ts を作るコマンドも扱います。

確認環境:Ubuntu 24.04、Node.js 22.22.2、TypeScript 7.0.2。版による違いは TypeScript 5.9.3・5.4.5・4.9.5 を別フォルダに入れて比べました。

JSDocタグの早見表

タグ 書き方 相当する TS 構文 確かめた版
@type /** @type {string} */ let s; let s: string 4.9.5〜7.0.2
@type(キャスト) /** @type {Item} */ (value) value as Item 4.9.5〜7.0.2
@param @param {number} [rate=0.1] 説明 (rate: number = 0.1) 4.9.5〜7.0.2
@returns @returns {number} ): number 4.9.5〜7.0.2
@typedef @typedef {object} Item + @property type Item = {...} 4.9.5〜7.0.2
@property @property {number} [qty] qty?: number 4.9.5〜7.0.2
@callback @callback Fn + @param @returns type Fn = (x: A) => B 4.9.5〜7.0.2
@template @template T 型引数 T を持つ関数・クラス 4.9.5〜7.0.2
@satisfies /** @satisfies {T} */ ({...}) {...} satisfies T 5.0 以降(4.9.5 では検査されない)
@overload @overload を並べたコメント 関数のオーバーロード 5.0 以降(4.9.5 では検査されない)
@import /** @import { Item } from "./types.js" */ import type { Item } from 5.5 以降(5.4.5 では TS2552)
@this @this {HTMLElement} function (this: HTMLElement) 4.9.5〜7.0.2
@extends /** @extends {Box<number>} */ 型引数つきの extends 4.9.5〜7.0.2
@implements /** @implements {Printable} */ implements Printable 4.9.5〜7.0.2
@readonly @private @protected @public プロパティの直前に書く 同名の修飾子 4.9.5〜7.0.2
@override メソッドの直前に書く override 4.9.5〜7.0.2
@deprecated /** @deprecated 代わりの名前 */ 同じ(.ts でも使える) 4.9.5〜7.0.2
@enum /** @enum {number} */ const P = {...} 値の型をそろえたオブジェクト 5.9.3 まで。7.0.2 では @typedef に置き換える
@constructor / @class 関数をクラスとして扱う class 5.9.3 まで。7.0.2 では class に置き換える

「確かめた版」は、後で載せるエラー用サンプル(ng.js)を各版の tsc にかけ、そのタグのエラーが出ることを見た結果です(4.9.5 と 5.4.5 では @import を @typedef {import(...)} に書き換えて実行)。@satisfies と @overload は TypeScript 5.0、@import は 5.5 のリリースノートに追加の記載があります。

型チェックを有効にする方法は3つあります。

方法 書く場所 対象
// @ts-check .js ファイルの先頭 そのファイルだけ
--checkJs("checkJs": true) コマンドまたは設定ファイル 対象の .js すべて
jsconfig.json フォルダの直下 エディタ用。tsc では -p jsconfig.json で指定

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JSDocでTypeScript型チェック|@type・@paramタグ一覧」

最小の手順:3行で試す

npm init -y
npm i -D typescript
npx tsc --checkJs --noEmit plain.js
// plain.js
/** @type {number} */
let count = "3";

function add(a, b) {
  return a + b;
}
add(1);
plain.js(2,5): error TS2322: Type 'string' is not assignable to type 'number'.
plain.js(4,14): error TS7006: Parameter 'a' implicitly has an 'any' type.
plain.js(4,17): error TS7006: Parameter 'b' implicitly has an 'any' type.

--noEmit は「ファイルを出力せず、チェックだけする」指定です。TypeScript 7.0.2 は strict が既定で有効なので、型を書いていない引数(a と b)も TS7006 として知らせてくれます。

タグを全部使ったサンプル(エラー0件)

型の定義だけを集めた types.js と、それを使う ok.js です。2つを同じフォルダ(src)に置きます。

// src/types.js
/**
 * @typedef {object} Item
 * @property {string} name 品名
 * @property {number} price 単価
 * @property {number} [qty] 数量(省略可)
 */

/** @typedef {"todo" | "doing" | "done"} Status */

/**
 * @callback PriceFilter
 * @param {Item} item
 * @returns {boolean}
 */

export {};
// src/ok.js
/** @import { Item, Status, PriceFilter } from "./types.js" */

// @type:変数の型
/** @type {string} */
let title = "備品リスト";

/** @type {Item[]} */
const items = [
  { name: "ノート", price: 300, qty: 2 },
  { name: "ペン", price: 120 },
];

// @param / @returns:引数と戻り値
/**
 * @param {Item[]} list
 * @param {number} [taxRate=0.1] 税率(省略可)
 * @returns {number}
 */
function total(list, taxRate = 0.1) {
  const sum = list.reduce((acc, i) => acc + i.price * (i.qty ?? 1), 0);
  return Math.round(sum * (1 + taxRate));
}

// @callback で定義した関数型を @type で使う
/** @type {PriceFilter} */
const isCheap = (item) => item.price < 200;

// @template:ジェネリクス
/**
 * @template T
 * @param {T[]} list
 * @returns {T | undefined}
 */
function first(list) {
  return list[0];
}

// @satisfies:型に合うか確かめつつ、推論された型を保つ
const labels = /** @satisfies {Record<Status, string>} */ ({
  todo: "未着手",
  doing: "作業中",
  done: "完了",
});

// @overload:呼び出し方ごとの型
/**
 * @overload
 * @param {string} value
 * @returns {string}
 */
/**
 * @overload
 * @param {number} value
 * @returns {number}
 */
/**
 * @param {string | number} value
 * @returns {string | number}
 */
function double(value) {
  return typeof value === "string" ? value + value : value * 2;
}

// 定数の集まりを型にする(as const と typeof の組み合わせ)
const Priority = /** @type {const} */ ({ LOW: 1, MIDDLE: 2, HIGH: 3 });
/** @typedef {typeof Priority[keyof typeof Priority]} PriorityValue */

/** @param {PriorityValue} p */
function priorityLabel(p) {
  return p === Priority.HIGH ? "高" : "通常";
}

// @this:this の型
/**
 * @this {HTMLElement}
 * @param {Event} e
 */
function onClick(e) {
  console.log(this.tagName, e.type);
}

// クラス:@extends・@implements・アクセス修飾子・@override
/**
 * @template T
 */
class Box {
  /** @param {T} value */
  constructor(value) {
    /** @readonly */
    this.value = value;
    /** @private */
    this.secret = "s";
    /** @protected */
    this.count = 0;
  }
  /** @returns {string} */
  describe() {
    return `Box(${this.value})`;
  }
}

/** @extends {Box<number>} */
class NumberBox extends Box {
  /** @override */
  describe() {
    return `NumberBox(${this.value.toFixed(1)})`;
  }
}

/** @typedef {{ print(): string }} Printable */
/** @implements {Printable} */
class Report {
  print() {
    return "report";
  }
}

// 型アサーション(キャスト)
const raw = /** @type {unknown} */ (JSON.parse('{"name":"消しゴム","price":80}'));
const parsed = /** @type {Item} */ (raw);

/** @deprecated total を使ってください */
function sum(/** @type {number[]} */ nums) {
  return nums.reduce((a, b) => a + b, 0);
}

console.log(title, total(items), items.filter(isCheap).length, first(items)?.name);
console.log(labels.done, double("ab"), double(21), priorityLabel(Priority.HIGH));
console.log(new NumberBox(3).describe(), new Report().print(), parsed.name, sum([1, 2]));
console.log(typeof onClick);

設定ファイル(jsconfig.json)と、package.json です。

{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "checkJs": true,
    "strict": true,
    "noEmit": true
  },
  "include": ["src"]
}
{ "type": "module" }
$ npx tsc -p jsconfig.json
$ node src/ok.js
備品リスト 792 1 ノート
完了 abab 42 高
NumberBox(3.0) report 消しゴム 3
function

tsc は何も表示せずに終わり(エラー0件)、同じファイルを Node.js でそのまま実行できました。コメントで型を書いているだけなので、変換の工程が要らないのが JSDoc 方式の持ち味です。

各タグの要点

タグ 要点
@type 変数の直前に書く。式の前に書いて丸かっこで囲むとキャストになる
@param [名前] で省略可、[名前=既定値] で既定値つき。{...number} で可変長、options.name の形で分割代入の中身も書ける
@typedef 型に名前を付ける。{object} + @property の形か、{"a" | "b"} のように TS の型をそのまま書く
@callback 関数の型に名前を付ける。@type {(a: number) => number} と直接書いてもよい
@template 型引数を宣言する。クラスに付けるとクラス全体で使える
@satisfies 型に合っているか確かめるだけで、変数の型は推論されたまま残る
@overload 1つのコメントが1つの呼び出し方。最後のコメントが実装の型
@import 型だけを読み込む。コメントなので実行時には何も読み込まれない

分割代入・可変長引数・Promise の例も、動く形で載せておきます。

// src/more.js
// @ts-check
/** @typedef {import("./types.js").Item} Item */

/**
 * @param {object} options
 * @param {string} options.name 名前
 * @param {number} [options.limit] 上限(省略可)
 * @param {...number} scores 点数(可変長)
 * @returns {Promise<Item>}
 */
async function build({ name, limit = 10 }, ...scores) {
  const price = Math.min(limit, scores.reduce((a, b) => a + b, 0));
  return { name, price };
}

/** @type {(a: number, b: number) => number} */
const add = (a, b) => a + b;

/** @type {Map<string, Item>} */
const stock = new Map();

build({ name: "ノート" }, 3, 4).then((item) => {
  stock.set(item.name, item);
  console.log(item, add(1, 2), stock.size);
});
$ npx tsc --checkJs --noEmit --strict --target es2022 --module nodenext src/more.js
$ node src/more.js
{ name: 'ノート', price: 7 } 3 1

1行目の @typedef {import("./types.js").Item} Item は、@import が入る前から使える書き方です。TypeScript 5.5 のリリースノートでは、この書き方を置き換えるものとして @import が紹介されています。

型エラーの全文:タグごとにわざと間違える

次の ng.js は、タグごとに1か所ずつ型に合わないコードを書いたものです。import している lib.js は「.jsから.d.tsを生成する」の節に全文があります。

// src/ng.js
/** @import { Item, Status, PriceFilter } from "./types.js" */
import { Box, NumberBox, total, first, double } from "./lib.js";

/** @type {string} */
let title = 123;

/** @type {Item} */
const item = { name: "ノート", price: "300" };

total([item], "0.1");

/** @type {PriceFilter} */
const isCheap = (item) => item.price;

/** @type {string | undefined} */
const s = first([1, 2, 3]);

const labels = /** @satisfies {Record<Status, string>} */ ({
  todo: "未着手",
  doing: "作業中",
});

double(true);

const box = new NumberBox(3);
box.value = 5;
box.secret;
box.count;

/** @typedef {{ print(): string }} Printable */
/** @implements {Printable} */
class Report {}

class Child extends Box {
  /** @override */
  descride() {
    return "";
  }
}

/**
 * @this {HTMLElement}
 */
function onClick() {
  console.log(this.tagNme);
}

/**
 * @param {string} nmae
 * @returns {number}
 */
function len(name) {
  return name;
}

/** @type {Status} */
const st = "finished";
$ npx tsc -p jsconfig.json
src/ng.js(5,5): error TS2322: Type 'number' is not assignable to type 'string'.
src/ng.js(8,29): error TS2322: Type 'string' is not assignable to type 'number'.
src/ng.js(10,15): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
src/ng.js(13,27): error TS2322: Type 'number' is not assignable to type 'boolean'.
src/ng.js(16,7): error TS2322: Type 'number | undefined' is not assignable to type 'string | undefined'.
  Type 'number' is not assignable to type 'string'.
src/ng.js(18,21): error TS2741: Property 'done' is missing in type '{ todo: string; doing: string; }' but required in type 'Record<Status, string>'.
src/ng.js(23,8): error TS2769: No overload matches this call.
  The last overload gave the following error.
    Argument of type 'boolean' is not assignable to parameter of type 'number'.
src/ng.js(26,5): error TS2540: Cannot assign to 'value' because it is a read-only property.
src/ng.js(27,5): error TS2341: Property 'secret' is private and only accessible within class 'Box<T>'.
src/ng.js(28,5): error TS2445: Property 'count' is protected and only accessible within class 'Box<T>' and its subclasses.
src/ng.js(32,7): error TS2420: Class 'Report' incorrectly implements interface 'Printable'.
  Property 'print' is missing in type 'Report' but required in type 'Printable'.
src/ng.js(34,21): error TS8026: Expected Box<T> type arguments; provide these with an '@extends' tag.
src/ng.js(36,3): error TS4123: This member cannot have a JSDoc comment with an 'override' tag because it is not declared in the base class 'Box<any>'. Did you mean 'describe'?
src/ng.js(45,20): error TS2551: Property 'tagNme' does not exist on type 'HTMLElement'. Did you mean 'tagName'?
src/ng.js(49,20): error TS8024: JSDoc '@param' tag has name 'nmae', but there is no parameter with that name.
src/ng.js(52,14): error TS7006: Parameter 'name' implicitly has an 'any' type.
src/ng.js(57,7): error TS2322: Type '"finished"' is not assignable to type 'Status'.
行 タグ TS番号 何を知らせているか
5 @type TS2322 string の変数に number を入れた
8 @typedef TS2322 price に文字列を入れた
10 @param TS2345 引数 taxRate に文字列を渡した
13 @callback TS2322 boolean を返すはずが number を返した
16 @template TS2322 first([1, 2, 3]) の戻り値は number | undefined
18 @satisfies TS2741 done が足りない
23 @overload TS2769 boolean を受け取る呼び出し方が無い
26 @readonly TS2540 読み取り専用のプロパティに代入した
27 @private TS2341 クラスの外から private に触れた
28 @protected TS2445 クラスの外から protected に触れた
32 @implements TS2420 print メソッドが無い
34 @extends TS8026 型引数つきのクラスを継承するときは @extends で型引数を書く
36 @override TS4123 親クラスに無い名前(describe の打ち間違い)
45 @this TS2551 tagName の打ち間違い。候補まで出る
49・52 @param TS8024・TS7006 コメントの引数名と実際の引数名が違う
57 @typedef TS2322 Status に無い文字列

npx tsc -p jsconfig.json --locale ja を付けると日本語で表示されます。たとえば5行目と18行目は次のとおりです。

src/ng.js(5,5): error TS2322: 型 'number' を型 'string' に割り当てることはできません。
src/ng.js(18,21): error TS2741: プロパティ 'done' は型 '{ todo: string; doing: string; }' にありませんが、型 'Record<Status, string>' では必須です。

TS8024 は JSDoc ならではのエラーです。コメントの @param の名前が実際の引数名と合っていないと、その引数は型なしの扱いになり、続けて TS7006 も出ます。引数名を変えたときはコメントも一緒に直します。

TypeScript 7.0で扱いが変わったタグ:@enumと@constructor

@enum と @constructor は、TypeScript 5.9.3 と 7.0.2 で結果が変わりました。同じファイルを両方の版にかけます。

// src/legacy.js
// @ts-check
/** @enum {number} */
const Priority = { LOW: 1, MIDDLE: "2", HIGH: 3 };

/** @param {Priority} p */
function setPriority(p) {}
setPriority("HIGH");

/**
 * @constructor
 * @param {string} name
 */
function Person(name) {
  this.name = name;
}
const person = new Person("佐藤");
person.nmae;

TypeScript 5.9.3 の結果:

src/legacy.js(3,28): error TS2322: Type 'string' is not assignable to type 'number'.
src/legacy.js(7,13): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
src/legacy.js(17,8): error TS2339: Property 'nmae' does not exist on type 'Person'.

TypeScript 7.0.2 の結果:

src/legacy.js(5,13): error TS2749: 'Priority' refers to a value, but is being used as a type here. Did you mean 'typeof Priority'?
src/legacy.js(14,3): error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.
src/legacy.js(16,16): error TS7009: 'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.
タグ 5.9.3 7.0.2
@enum 値の型をチェックし、型名としても使える 型名として使うと TS2749。値の型(MIDDLE: "2")は指摘されない
@constructor 関数を new でき、プロパティもチェックされる TS2683 と TS7009

公式ブログの TypeScript 7.0 の告知にも、JavaScript ファイルの解析を TypeScript ファイルと同じ考え方にそろえた結果として、@enum は認識されなくなり @typedef を使うこと、@class を付けた関数はコンストラクタとして扱われなくなり class 宣言を使うことが書かれています。7.0.2 での置き換え方は次のとおりです。

// src/enum7.js
// @ts-check
const Priority = /** @type {const} */ ({ LOW: 1, MIDDLE: 2, HIGH: 3 });
/** @typedef {typeof Priority[keyof typeof Priority]} PriorityValue */

/** @param {PriorityValue} p */
function setPriority(p) {
  console.log("priority:", p);
}
setPriority(Priority.HIGH);
setPriority(4);

class Person {
  /** @param {string} name */
  constructor(name) {
    this.name = name;
  }
}
new Person("佐藤").nmae;
$ npx tsc --checkJs --noEmit --strict --target es2022 --module nodenext src/enum7.js
src/enum7.js(10,13): error TS2345: Argument of type '4' is not assignable to parameter of type 'PriorityValue'.
src/enum7.js(18,18): error TS2339: Property 'nmae' does not exist on type 'Person'.

/** @type {const} */ (...) は TypeScript の as const にあたり、PriorityValue は 1 | 2 | 3 になります。5.9.3 の @enum {number} では number なら何でも通りましたが、この書き方は 定義した値だけを受け付けるので、setPriority(4) も知らせてくれます。

版による違い:@satisfies・@overload・@import

同じファイルを、以前の版の tsc にもかけて比べました。

// src/old.js
// @ts-check
const labels = /** @satisfies {Record<"todo" | "done", string>} */ ({
  todo: "未着手",
});

/**
 * @overload
 * @param {string} value
 * @returns {string}
 */
/**
 * @overload
 * @param {number} value
 * @returns {number}
 */
/**
 * @param {string | number} value
 * @returns {string | number}
 */
function double(value) {
  return typeof value === "string" ? value + value : value * 2;
}
/** @type {number} */
const n = double("ab");
版 2行目(@satisfies) 24行目(@overload)
4.9.5 エラーなし(検査されない) TS2322: Type 'string | number' is not assignable to type 'number'
5.9.3 TS1360(done が足りない) TS2322: Type 'string' is not assignable to type 'number'
7.0.2 TS2741(done が足りない) TS2322: Type 'string' is not assignable to type 'number'

4.9.5 では @overload が読まれず、実装の型(string | number)がそのまま戻り値になっています。5.9.3 と 7.0.2 では「文字列を渡したら文字列が返る」ことまで伝わっています。7.0.2 の出力全文です。

src/old.js(2,21): error TS2741: Property 'done' is missing in type '{ todo: string; }' but required in type 'Record<"done" | "todo", string>'.
src/old.js(24,7): error TS2322: Type 'string' is not assignable to type 'number'.

@import は、次のファイルで比べました。

// src/imp.js
// @ts-check
/** @import { Item } from "./types.js" */

/** @type {Item} */
const item = { name: "ノート", price: 300 };
console.log(item.name);
版 結果
5.4.5 src/imp.js(4,12): error TS2552: Cannot find name 'Item'. Did you mean 'item'?
5.9.3 エラーなし
7.0.2 エラーなし

5.4 以前の TypeScript を使っている場合は、/** @typedef {import("./types.js").Item} Item */ と書けば同じことができます。

ts-check・checkJs・jsconfig.jsonの違い

最初の plain.js(// @ts-check なし)と、先頭に // @ts-check を足した checked.js、// @ts-nocheck を足した nocheck.js を、オプションを変えてチェックしました。

コマンド 対象 結果
tsc --noEmit plain.js 何も付けない TS6504(.js は対象外。allowJs を付けるよう案内される)
tsc --allowJs --noEmit plain.js allowJs だけ エラー0件(読み込むだけでチェックしない)
tsc --allowJs --noEmit checked.js allowJs + // @ts-check 3件(TS2322、TS7006 が2件)
tsc --checkJs --noEmit plain.js checkJs 3件(同上)
tsc --allowJs --checkJs --strict false --noEmit plain.js checkJs + strict を外す 1件(TS2322 のみ)
tsc --checkJs --noEmit nocheck.js checkJs + // @ts-nocheck エラー0件

何も付けない場合のメッセージは次のとおりです(1件分)。

error TS6504: File 'plain.js' is a JavaScript file. Did you mean to enable the 'allowJs' option?
  The file is in the program because:
    Root file specified for compilation
指定 役割 使いどころ
allowJs .js を tsc の対象に入れる(チェックはしない) .ts と .js が混ざっている
checkJs 対象の .js をすべてチェックする 全体をチェックしたい
// @ts-check checkJs なしでも、そのファイルだけチェックする 1ファイルずつ広げたい
// @ts-nocheck checkJs ありでも、そのファイルだけ外す 生成されたファイルなど
strict 型を書いていない引数(TS7006)なども知らせる 型をしっかり書く方針のとき

1ファイルずつ始めるなら // @ts-check、全体に広げるなら checkJs + 例外に // @ts-nocheck という使い分けができます。型を書く量を抑えて始めたいときは、strict を外すと「書いた型と食い違っている箇所」だけに絞れます。

jsconfig.jsonはtscでは-pで指定する

jsconfig.json は JavaScript プロジェクト用の設定ファイルで、中身の書き方は tsconfig.json と同じです。tsc を引数なしで実行したときに自動で読まれるのは tsconfig.json のほうで、jsconfig.json だけを置いたフォルダで npx tsc を実行すると、チェックは行われずヘルプが表示されました。-p で指定するとチェックされます。

$ npx tsc -p jsconfig.json
plain.js(2,5): error TS2322: Type 'string' is not assignable to type 'number'.
plain.js(4,14): error TS7006: Parameter 'a' implicitly has an 'any' type.
plain.js(4,17): error TS7006: Parameter 'b' implicitly has an 'any' type.

package.json の scripts に "typecheck": "tsc -p jsconfig.json" と書いておくと、npm run typecheck で呼べます。

1行だけ除外する:@ts-expect-errorと@ts-ignore

// sup.js
// @ts-check
/** @type {number} */
let a = 1;

// @ts-expect-error 文字列を入れるテスト
a = "x";

// @ts-ignore
a = "y";

// @ts-expect-error
a = 2;
$ npx tsc --allowJs --noEmit sup.js
sup.js(11,1): error TS2578: Unused '@ts-expect-error' directive.
コメント 次の行にエラーがあるとき 次の行にエラーが無いとき
// @ts-expect-error 表示しない TS2578 で知らせる
// @ts-ignore 表示しない 何も起きない

@ts-expect-error は、コードが直ってエラーが無くなると「もう要りません」と教えてくれるので、除外のコメントが残り続けません。

.jsから.d.tsを生成する

JSDoc を書いた .js からは、型定義ファイル(.d.ts)を tsc で作れます。ライブラリとして配るときに、利用する側の TypeScript が型を読めるようになります。

// src/lib.js
/** @import { Item } from "./types.js" */

/**
 * @param {Item[]} list
 * @param {number} [taxRate=0.1]
 * @returns {number}
 */
export function total(list, taxRate = 0.1) {
  const sum = list.reduce((acc, i) => acc + i.price * (i.qty ?? 1), 0);
  return Math.round(sum * (1 + taxRate));
}

/**
 * @template T
 * @param {T[]} list
 * @returns {T | undefined}
 */
export function first(list) {
  return list[0];
}

/**
 * @overload
 * @param {string} value
 * @returns {string}
 */
/**
 * @overload
 * @param {number} value
 * @returns {number}
 */
/**
 * @param {string | number} value
 * @returns {string | number}
 */
export function double(value) {
  return typeof value === "string" ? value + value : value * 2;
}

export const Priority = /** @type {const} */ ({ LOW: 1, MIDDLE: 2, HIGH: 3 });
/** @typedef {typeof Priority[keyof typeof Priority]} PriorityValue */

/**
 * @template T
 */
export class Box {
  /** @param {T} value */
  constructor(value) {
    /** @readonly */
    this.value = value;
    /** @private */
    this.secret = "s";
    /** @protected */
    this.count = 0;
  }
  /** @returns {string} */
  describe() {
    return `Box(${this.value})`;
  }
}

/** @extends {Box<number>} */
export class NumberBox extends Box {
  /** @override */
  describe() {
    return `NumberBox(${this.value.toFixed(1)})`;
  }
}

/** @deprecated total を使ってください */
export function sum(/** @type {number[]} */ nums) {
  return nums.reduce((a, b) => a + b, 0);
}
npx tsc src/lib.js src/types.js --allowJs --declaration --emitDeclarationOnly --outDir dts --target es2022 --module nodenext

出力された dts/lib.d.ts:

/** @import { Item } from "./types.js" */
import type { Item } from "./types.js";
/**
 * @param {Item[]} list
 * @param {number} [taxRate=0.1]
 * @returns {number}
 */
export declare function total(list: Item[], taxRate?: number): number;
/**
 * @template T
 * @param {T[]} list
 * @returns {T | undefined}
 */
export declare function first<T>(list: T[]): T | undefined;
export declare function double(value: string): string;
export declare function double(value: number): number;
export declare const Priority: {
    readonly LOW: 1;
    readonly MIDDLE: 2;
    readonly HIGH: 3;
};
export type PriorityValue = typeof Priority[keyof typeof Priority];
/** @typedef {typeof Priority[keyof typeof Priority]} PriorityValue */
/**
 * @template T
 */
export declare class Box<T> {
    /** @readonly */
    value: T;
    /** @private */
    secret;
    /** @protected */
    count: number;
    /** @param {T} value */
    constructor(value: T);
    /** @returns {string} */
    describe(): string;
}
/** @extends {Box<number>} */
export declare class NumberBox extends Box<number> {
    /** @override */
    describe(): string;
}
/** @deprecated total を使ってください */
export declare function sum(/** @type {number[]} */ nums: number[]): number;

出力された dts/types.d.ts:

/**
 * @typedef {object} Item
 * @property {string} name 品名
 * @property {number} price 単価
 * @property {number} [qty] 数量(省略可)
 */
export type Item = {
    /**
     * 品名
     */
    name: string;
    /**
     * 単価
     */
    price: number;
    /**
     * 数量(省略可)
     */
    qty?: number;
};
export type Status = "todo" | "doing" | "done";
export type PriceFilter = (item: Item) => boolean;
/** @typedef {"todo" | "doing" | "done"} Status */
/**
 * @callback PriceFilter
 * @param {Item} item
 * @returns {boolean}
 */
export {};
  • @import は import type に、@template T は型引数つきの宣言(first や Box の直後に T が付いた形)に変わっている
  • @overload の2つのコメントは、2行の export declare function double になっている
  • [taxRate=0.1] は taxRate?: number、@property {number} [qty] は qty?: number になっている
  • @typedef と @callback は export type になり、@property の説明文もコメントとして残っている

JSDocと.tsのどちらを選ぶか

状況 向いている方法
ビルドなしで動かしたい小さなスクリプトや設定ファイル JSDoc + // @ts-check
既存の JavaScript に少しずつ型を足したい JSDoc + checkJs(strict は後から)
型をたくさん定義する/ジェネリクスを多用する .ts(型の記述が短く済む)
新しく始める TypeScript 前提の開発 .ts

JSDoc で書いた型は .d.ts にもそのまま変換できるので、あとから .ts へ移す場合も、書いた型の情報はそのまま活かせます。

よくある質問

Q. JSDoc の型チェックに TypeScript のインストールは必要ですか? A. コマンドでチェックするには npm i -D typescript で tsc を入れます。.js ファイル自体は変換せずにそのまま動くので、実行時に TypeScript は要りません。

Q. // @ts-check と checkJs はどちらを使えばよいですか? A. 1ファイルずつ試すなら // @ts-check、フォルダ全体をチェックするなら設定ファイルに "checkJs": true を書きます。両方を同時に使っても問題ありません。

Q. @enum を書いたら TS2749 になりました。 A. TypeScript 7.0.2 では @enum が型名として扱われません。/** @type {const} */ ({...}) で定数を作り、@typedef {typeof X[keyof typeof X]} で型を作る形に置き換えると、定義した値だけを受け付ける型になります。

Q. 型を別ファイルにまとめるにはどうしますか? A. @typedef と @callback を1つの .js にまとめ、最後に export {}; を書きます。使う側は /** @import { Item } from "./types.js" */ で読み込みます(TypeScript 5.5 以降)。

まとめ

  • JSDoc で型を書いた .js は tsc --checkJs --noEmit でそのまま型チェックできる
  • 基本は @type・@param・@returns・@typedef の4つ。関数の型は @callback、ジェネリクスは @template
  • @satisfies と @overload は 5.0 以降、@import は 5.5 以降。以前の版では検査されないかエラーになる
  • @enum と @constructor は 7.0.2 で扱いが変わった。@typedef と class に置き換える
  • 1ファイルなら // @ts-check、全体なら checkJs。jsconfig.json は tsc -p jsconfig.json で指定する
  • コメントの引数名が実際と違うと TS8024。引数名を変えたらコメントも直す
  • tsc --allowJs --declaration --emitDeclarationOnly で .js から .d.ts を作れる

参考・出典

  • TypeScript 公式ハンドブック「JSDoc Reference」 https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html
  • TypeScript 公式ハンドブック「JS Projects Utilizing TypeScript」 https://www.typescriptlang.org/docs/handbook/intro-to-js-ts.html
  • TypeScript 5.0 リリースノート(@satisfies / @overload Support in JSDoc) https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html
  • TypeScript 5.5 リリースノート(The JSDoc @import Tag) https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html
  • Announcing TypeScript 7.0(TypeScript 公式ブログ) https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JSDocでTypeScript型チェック|@type・@paramタグ一覧」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?