TypeScriptのJSDocサポートでできること、できないこと
TypeScriptの主要な入力ファイルは .ts
, .tsx
, .mts
, .cts
ですが、JavaScriptファイル (.js
, .jsx
, .mjs
, .cjs
) も読み込んで処理することができます。JSDocによる型アノテーションを認識するため、生のJavaScriptでもそれなりに型をつけることができます。
本稿ではタイトル通り、TypeScriptのJSDocサポートでできることとできないこと (.ts
でしかできないこと) をまとめます。
おことわり
- 本記事はTypeScript 4.4時点での実装状況に基づいています。なるべくソースコード中の関係する箇所を参照するようにしたので、今後の変更はご自分で検証してください。 (TypeScript Playgroundで試すだけでも有用です JavaScriptモードで開始できるリンク)
- JSDocの機能一覧・TypeScriptの独自機能一覧は、筆者が実装中を丹念に探索して列挙したものです。少なくとも公式ドキュメンテーションよりも網羅性が高くなるようにつとめていますが、100%の網羅性を保証するものではありません。同じ理由で、「JSDocでは実現できない」と記載しているものについても見落としがある可能性はあります。
総評
TypeScriptコンパイラ (tsc) はTypeScriptファイル (.ts) だけではなくJavaScriptファイル (.js) を入力にとることも可能で、設定次第では型検査を行うこともできます。しかしTypeScriptとJavaScriptで同じ機能が使えるわけではありません。TypeScript 4.4時点で、TypeScript側にのみある機能として以下があります。
- 書きやすい型アノテーション
- TypeScript独自機能 (namespace, enum, constructor parameter modifiers)
- JSDocでは書けない各種アノテーション
- 関数のオーバーロード宣言
- 関数呼び出し、コンストラクタ呼び出し、タグ付きテンプレートリテラル、JSXの型実引数指定
- interface宣言
- 型に対するimport/export宣言 (型と値がセットの場合はJavaScriptでも可能)
- アンビエント宣言 (クラスフィールド、変数、モジュール) およびモジュール拡張
- クラスフィールドの一部アノテーション (optional field, フィールドの存在表明, declare, abstract)
- クラスのindex signature
- abstract class
- as const
- non-null表明
- 型仮引数 (ジェネリクス引数) のデフォルト値
- 推論できなかったジェネリクス引数のデフォルトが
unknown
になる- JavaScriptモードでは
any
になってしまう。
- JavaScriptモードでは
JavaScript側だけの機能として以下があります。
- JSDoc互換の型アノテーション (TypeScriptモードでは無効)
- 総じてTypeScriptほど強力ではなく、ドキュメント目的を兼ねるのでなければ記述量が増えるデメリットがある
- あくまで(本来の)JSDocの表現力の範疇で推論のヒントとするように設計されているという印象
- 型推論における、より積極的なヒューリスティックス
- 名前空間パターンの推論
- コンストラクタ関数の推論
- this代入によるクラスフィールドの推論
- プロトタイプ代入によるクラスフィールドの推論
- CommonJS require/exports推論
-
<>
の曖昧性があるときにJavaScriptの規格に準拠してパースされる
このようにして見てみると、tscのJavaScript対応は単なるTypeScript対応の下位互換ではなく、レガシーコード対応により特化しているようにも思えます。こういった傾向も踏まえた上で、個々の能力差も参考にしながら言語を選択するといいのではないかと思います。
TypeScriptコンパイラがサポートするファイルタイプ
TypeScript 4.4時点で、TypeScriptコンパイラは .ts
, .tsx
, .d.ts
, .js
, .jsx
, .json
の6個の拡張子を認識します。
TypeScript 4.5時点で、TypeScriptコンパイラは .ts
, .tsx
, .mts
, .cts
, .d.ts
, .d.mts
, .d.cts
, .js
, .jsx
, .mjs
, .cjs
, .json
の12個の拡張子を認識します。
デフォルト | ESM | CJS | JSX | JSX+ESM | JSX+CJS | |
---|---|---|---|---|---|---|
TypeScript | .ts |
.mts |
.cts |
.tsx |
||
型定義 | .d.ts |
.d.mts |
.d.cts |
|||
JavaScript | .js |
.mjs |
.mts |
.jsx |
||
JSON | .json |
このうちJavaScriptに分類されるファイルは allowJs
が有効なときだけ探索対象に含まれます。 (allowJs
はcheckJs
指定時にデフォルトで有効化されます)
TypeScriptは途中で型エラーや不整合があっても構わず型チェックを進められるように実装されています。途中で得られた型エラーは必ず報告されるわけではなく、ファイルタイプごとのデフォルトは以下のようになっています。
- TypeScript: 型エラーを報告する。
- 型定義:
skipLibCheck
が指定されていなければ、型エラーを報告する。 - JavaScript:
checkJs
が指定されていれば、型エラーを報告する。
TypeScript, JavaScript の場合は // @ts-check
/ // @ts-nocheck
マジックコメントによって上記のデフォルト挙動を上書きすることができます。
TypeScriptのJSDocサポートについて
JavaScriptファイル内ではTypeScript特有の構文は使えないかわりにJSDoc互換のコメントで型アノテーション・型定義を与えることができます。
// @ts-check
/**
* @param x {number}
* @return {number}
*/
export function square(x) {
return x * x;
}
TypeScriptファイル内ではJSDocコメントは無視されますが、IDEの機能 (tsserverの機能) でJSDocコメントの型情報をTypeScriptの型アノテーションに変換することができます。
JSDoc内で型を指定するときはTypeScriptの構文がそのまま使えますが、それに加えてJSDoc互換の記法がいくつかサポートされています。これについては後述します。
なお、本来JSDocはドキュメンテーション生成ツールの名称ですが、ここではTypeScriptのJSDoc互換コメントの処理のことをJSDocと呼んでいます。この2つは本来ならば区別されべきものです。 (たとえば#14377のコメントを参照)
JSDocタグ一覧
型システムに関係のあるタグとして以下が認識されます。
JSDoc | 用途 |
---|---|
@type {T} @type T
|
型アノテーション |
@this {T} @this T
|
this 引数型アノテーション |
@enum {T} @enum T
|
Closure互換enum |
@param {T} Name @param Name {T} (別名: @arg , @argument ) |
引数型アノテーション |
@param {T} [Name] @param [Name] {T} (別名: @arg , @argument ) |
オプショナル引数型アノテーション |
@property {T} Name @property Name {T} (別名: @prop ) |
@typedef のプロパティ宣言 |
@property {T} [Name] @property [Name] {T} (別名: @prop ) |
@typedef のオプショナルプロパティ宣言 |
@returns {T} (別名: @return ) |
戻り値型アノテーション |
@returns (別名: @return ) |
型がない場合は特別な効果はない |
@typedef {T} Name |
型エイリアス (単独) |
@typedef {Object} Name @typedef {object} Name @typedef {T[]} Name
|
型エイリアス (プロパティ宣言が後続可能) |
@typedef Name |
型エイリアス (プロパティ宣言が後続する) |
@callback Name |
関数型エイリアス (@param /@returns が後続する) |
@template T0,T1,T2 |
ジェネリクス仮引数 |
@template {T} T0,T1,T2 |
ジェネリクス仮引数、制約つき |
@implements {Iface<T>} @implements Iface<T>
|
クラスのimplements表明 |
@extends {Klass<T>} @extends Klass<T> (別名: @augments ) |
クラスのextendsのジェネリクス引数指定 |
@constructor (別名: @class ) |
コンストラクタ関数であることの表明 |
@public |
クラスメンバーの可視性 (public) |
@private |
クラスメンバーの可視性 (private) |
@protected |
クラスメンバーの可視性 (protected) |
@readonly |
クラスフィールド等の読み取り専用表明 |
@override |
クラスメンバーのオーバーライド表明 |
型システムに関係ないタグとして以下があります。
-
@deprecated
- IDEによる非推奨表示 (取消線で修飾するなど) のために使われる。
-
@author
-
<>
で囲うと@
がパースされなくなるという特殊ルールのために実装されている。
-
-
@see
- 他の定義へのリンクが解釈される。
-
@link
- 他のタグの中で
{@link ...}
のようにして使う。 - 他の定義へのリンクが解釈される。
- 他のタグの中で
- それ以外のタグ (
@abstract
,@event
,@async
など)- このような形になっていればパースだけ行われる。
以下の指令はJSDocのようにも見えますが、実際にはdoc-comment形式 (/** ... */
) でなくても受理されます。
-
@ts-check
,@ts-nocheck
(1行コメント形式のみ有効)- そのファイルの型エラーを出力するかどうかの制御。
-
@ts-ignore
,@ts-expect-error
- 次の行の型エラーを無視する。
-
@jsx
,@jsxFrag
,@jsxImportSource
,@jsxRuntime
(複数行コメント形式のみ有効)- React以外のカスタムJSX実装を使うときに指定する。
-
@internal
- 指定した定義を
.d.ts
に含めない。 (--stripInternal
指定時に有効)
- 指定した定義を
TypeScriptの独自機能
TypeScriptは基本的にはJavaScriptのランタイム機能に型定義と型アノテーションだけを乗せるような設計になっていますが、いくつかJavaScript側には存在しない機能があります。
namespace
namespace
または module
を使うことで名前空間を定義できます。
// module util でもよい
namespace util {
export function square(x: number) {
return x * x;
}
}
JavaScriptのランタイム上は、名前空間はプレーンなオブジェクトの入った変数として扱われます。同名のnamespaceを複数回書くとマージされます。
TypeScript的には名前空間はファイルに紐付かない点以外はモジュールと同じように扱われます。名前空間内にはinterfaceやtypeなど型システム上にしか存在しない実体も入れることができます。
namespace util {
export type Status = "opened" | "closed";
export let status: Status = "opened";
}
名前空間からexportされた値や型は、import ... =
構文 (Legacy importの一種) でインポートできます。
namespace util {
export type Status = "opened" | "closed";
export let status: Status = "opened";
}
namespace foo {
import StatusType = util.Status;
import status = util.status;
console.log(status);
}
enum
Cのenumと同様の機能です。デフォルトでは連番で値が振られます。
enum Foo {
HTML,
JS,
JSON,
PNG
}
ランタイム上は以下のようなオブジェクトになります。 (マージ処理は省略)
var Foo = {
HTML: 0,
JS: 1,
JSON: 2,
PNG: 3,
0: "HTML",
1: "JS",
2: "JSON",
3: "PNG",
};
またenumは同名の型定義をともないます。おおよそ以下と同等です。
type Foo = 0 | 1 | 2 | 3; // より緩くnumberとして扱われることもある
JSDocの @enum
はClosure Compiler互換のために存在し、TypeScriptのenumとは挙動が異なります。
const enum
const enum
は enum
とよく似ていますが、enum objectが定義されません。必ず Foo.HTML
のようにenumの値を直接指す必要があります。
const enum Foo {
HTML,
JS,
JSON,
PNG
}
// console.log(Foo); // error
console.log(Foo.HTML); // 0
const enumはコンパイル時に Foo.HTML
のような参照が定数値に置換されます。しかし、これを行うには別モジュールの型情報に基づいた置換が必要になるため、 --isolatedModules
がオンのときは単なるenumとしてコンパイルされます。Babel (7.15.0以降) も同様です。
コンストラクタのパラメタ修飾子
コンストラクタ引数にpublic/protected/private/readonly/overrideなどの修飾子をつけることができます。修飾子が1つ以上ついている場合は同名のクラスフィールドが自動的に作られ、当該引数で初期化されます。
class PackageVersion {
constructor(public readonly pkgname: string, public readonly version: string) {}
}
これは以下と同等です。
class PackageVersion {
public readonly pkgname: string;
public readonly version: string;
constructor(pkgname: string, version: string) {
this.pkgname = pkgname;
this.version = version;
}
}
レガシーデコレーター
メソッドなどの宣言に @something
のような記法を前置することで挙動を変えたり情報を付与したりする仕組みです。将来的にECMAScriptに入る可能性がありますが、現在のTypeScriptの実装は古い提案に基づくため事実上の独自機能と言えます。詳しくは@petamoriken氏のESNext Stage 2 Decorators の変遷と最新仕様を参照してください。
import / export の消去
ここまで紹介した「独自機能」とは毛色が違いますが、TypeScriptファイルに対して行われる追加の変換として「import / exportの消去」があります。たとえば以下の例を考えます。
import React from "react";
import { FC } from "react";
export const Hello: FC = () => <div>Hello, world!</div>;
このとき FC
はReactのJavaScript側コードには存在せず、 @types/react
内のinterfaceとしてのみ存在します。JavaScriptにトランスパイルするときにこれをそのまま残してしまうと、存在しないインポートとなってしまい、処理系のES Modulesの扱いによってはエラーになってしまいます。
これに対応するため、 import / export は一定ルールで消去されます。その詳細についてはBabel/TypeScript間の微妙な差異も含めて難しい点が色々存在するので本記事では深入りしません。
TypeScript 3.8以降では import type
/ export type
宣言が使えるようになったため、これらの難しい挙動に依存せずにコードを書くこともできるようになりました。
アノテーション系構文の詳細
let/const/varの型アノテーション
const x: number = 42;
// ^^^^^^^^
let y: number = 42;
// ^^^^^^^^
var z: number = 42;
// ^^^^^^^^
for (let i: number = 0; i < 10; i++) {}
// ^^^^^^^^
for (var j: number = 0; j < 10; j++) {}
// ^^^^^^^^
シンプルなlet/const/varの場合は、通常以下のように型アノテーションを書きます。
/** @type {number} */
const x = 42;
/** @type {number} */
let y = 42;
/** @type {number} */
var z = 42;
2つ以上の宣言子がある場合は、上のような @type
アノテーションは最初の宣言子に対してのみ適用されます。宣言子の直前の @type
も認識されるため、これで対応することができます。
/** @type {number} */
const a = 42, /** @type {number} */ b = 42;
同じ方法でforの型アノテーションを与えられます。
for (let /** @type {number} */ i = 0; i < 10; i++) {}
for (var /** @type {number} */ j = 0; j < 10; j++) {}
catch引数アノテーション
catchの引数には any
または unknown
が指定できます。
try {} catch(e: any) {}
// ^^^^^
try {} catch(e: unknown) {}
// ^^^^^^^^^
JavaScriptでは @type
で同様の指定ができます。
try {} catch (/** @type {any} */ e) {}
try {} catch (/** @type {unknown} */ e) {}
関数・メソッドの引数型アノテーション
function f1(x: number) {}
// ^^^^^^^^
const f2 = function(x: number) {};
// ^^^^^^^^
const f3 = (x: number) => {};
// ^^^^^^^^
class C {
meth(x: number) {}
// ^^^^^^^^
set something(value: number) {}
// ^^^^^^^^
}
@param
(別名: @arg
, @argument
) を使うのが一般的です。
/** @param {number} x */
function f1(x) {}
/** @param {number} x */
const f2 = function(x) {};
/** @param {number} x */
const f3 = (x) => {};
class C {
/** @param {number} x */
meth(x) {}
/** @param {number} value */
set something(value) {}
}
関数定義に対しては @type
を指定することでも間接的に引数型を指定することができます。
/** @type {function(number): void} */
function f1(x) {}
/** @type {function(number): void} */
const f2 = function(x) {};
/** @type {function(number): void} */
const f3 = (x) => {};
関数・メソッドのthis引数型アノテーション
function f1(this: number[]) {}
// ^^^^^^^^^^^^^^
const f2 = function(this: number[]) {};
// ^^^^^^^^^^^^^^
class C {
meth(this: number[]) {}
// ^^^^^^^^^^^^^^
}
@this
アノテーションを使うことができます。
/** @this {number[]} */
function f1() {}
/** @this {number[]} */
const f2 = function() {};
class C {
/** @this {number[]} */
meth() {}
}
@type
を指定することでも間接的にthis引数型を指定することができます。
/** @type {function(this: number[]): void} */
function f1() {}
/** @type {function(this: number[]): void} */
const f2 = function() {};
class C {
/** @type {function(this: number[]): void} */
meth() { const x = this; }
}
関数・メソッドのオプショナル引数アノテーション
function f1(x?: number) {}
// ^
const f2 = function(x?: number) {};
// ^
const f3 = (x?: number) => {};
// ^
class C {
meth(x?: number) {}
// ^
}
@param
(別名: @arg
, @argument
) の名前を []
で囲うことでオプショナル引数を表明できます。
/** @param {number} [x] */
function f1(x) {}
/** @param {number} [x] */
const f2 = function(x) {};
/** @param {number} [x] */
const f3 = (x) => {};
class C {
/** @param {number} [x] */
meth(x) {}
}
型に =
をつけてもオプショナルになります。
/** @param {number=} x */
function f1(x) {}
/** @param {number=} x */
const f2 = function(x) {};
/** @param {number=} x */
const f3 = (x) => {};
class C {
/** @param {number=} x */
meth(x) {}
}
@type
で関数型を指定した場合、TypeScript 4.4時点ではオプショナルにはならないようです。
関数・メソッドの戻り値型アノテーション
function f1(): number { return 42; }
// ^^^^^^^^
const f2 = function(): number { return 42; };
// ^^^^^^^^
const f3 = (): number => 42;
// ^^^^^^^^
class C {
meth(): number { return 42; }
// ^^^^^^^^
get something(): number { return 42; }
// ^^^^^^^^
}
@returns
(別名: @return
) でアノテーションできます。
/** @returns {number} */
function f1() { return 42; }
/** @returns {number} */
const f2 = function() { return 42; };
/** @returns {number} */
const f3 = () => 42;
class C {
/** @returns {number} */
meth() { return 42; }
/** @returns {number} */
get something() { return 42; }
}
@type
に関数型を指定することでもアノテーションできます。
/** @type {function(): number} */
function f1() { return 42; }
/** @type {function(): number} */
const f2 = function() { return 42; };
/** @type {function(): number} */
const f3 = () => 42;
class C {
/** @type {function(): number} */
meth() { return 42; }
/** @type {function(): number} */
get something() { return 42; }
}
クラスフィールド型アノテーション
class C {
foo: number = 42;
// ^^^^^^^^
}
@type
が使えます。
class C {
/** @type {number} */
foo = 42;
}
アンビエントクラスフィールド型アノテーション
ECMAScript仕様上、「初期化子のないクラスフィールド宣言」と「宣言がない状態」は厳密には異なります。TypeScriptで型アノテーションをしつつクラスフィールド宣言を出力させたくない場合は declare
を使います。
class C {
declare foo: number;
}
JavaScriptで同等のアノテーションをするのはやや面倒です。
コンストラクタ内で初期化している場合は初期化処理が代入宣言とみなされるため、そこに @type
でアノテーションをすることができます。 (コンストラクタ以外のメソッド内でも使えますが、optionalになってしまいます)
class C {
constructor() {
/** @type number */
this.foo = 42;
}
}
または、プロトタイプ代入宣言をつける手もありますが、コードに謎の式文が残ってしまうという点で厳密には元のコードと等価ではありません。
class C {
}
/** @type number */
C.prototype.foo;
クラスフィールド・メソッドのoptionalアノテーション
class C {
foo?: number;
// ^
meth?() {}
// ^
}
調べた範囲内では、同等のアノテーションをJSDocで行うことは難しそうです。
クラスフィールドの存在表明
class C {
foo!: number;
// ^
}
調べた範囲内では、同等のアノテーションをJSDocで行うことは難しそうです。
クラスフィールド・メソッドの修飾子 (public/private/protected/readonly/declare/abstract/override)
class S {
foo7: number | string = 42;
meth5() {}
}
abstract class C extends S {
public foo1: number = 42;
protected foo2: number = 42;
private foo3: number = 42;
readonly foo4: number = 42;
declare foo5: number;
abstract foo6: number;
override foo7: number = 42;
public meth1() {}
protected meth2() {}
private meth3() {}
abstract meth4(): void;
override meth5() {}
}
declare
, abstract
以外は同名のJSDocタグが認識されます。
class S {
/** @type {number | string} */
foo7 = 42;
meth5() {}
}
// abstract classに対応するJSDoc実装はない
class C extends S {
// 以降簡易的に複数のJSDocを1行にまとめている (一般的な書き方ではないので注意)
/** @public @type {number} */
foo1 = 42;
/** @protected @type {number} */
foo2 = 42;
/** @private @type {number} */
foo3 = 42;
/** @readonly @type {number} */
foo4 = 42;
// declare foo5: number;
// abstract foo6: number;
/** @override @type {number} */
foo7 = 42;
/** @public */
meth1() {}
/** @protected */
meth2() {}
/** @private */
meth3() {}
// abstract meth4(): void;
/** @override */
meth5() {}
}
declare
は前述の通り、別の箇所で代入宣言として書くことができれば同等の型アノテーションを再現することができます。 abstract
は探した限りでは対応する機能は (今のところ) なさそうです。
クラスのindex signature
class C {
[key: number]: string;
}
JSDocで等価な宣言を行う方法はおそらくありません。
as / レガシー型表明
const arr = ["a", 42].filter((x) => typeof x === "string");
const arr2 = arr as string[];
// ^^^^^^^^^^^
const arr3 = <string[]>arr; // JSX有効時は使えない
// ^^^^^^^^^^
e as T
と <T>e
は同じものです (<T>e
はJSXと思い切り衝突するため現在はあまり使われない)
JSDocで括弧式に @type
アノテーションをつけると as
と同等に扱われます。
const arr = ["a", 42].filter((x) => typeof x === "string");
const arr2 = /** @type {string[]} */ (arr); // 括弧が重要
as const
const x = { OPEN: "open" } as const;
as const
は内部的には const
を型名としてパースしたあと型チェッカーで特別扱いしています。TypeScript 4.5で@type
を使って同じことができるようになりました。
const x = /** @type {const} */ ({ OPEN: "open" });
non-null表明
const match = /a|$/.exec("abc")!;
// ^
console.log(match[0]);
JSDocで同等の表明は今のところできなさそうです。通常のasは使えるので、それである程度までは代用が可能です。
// asで代用
const match = /** @type {RegExpExecArray} */ (/a|$/.exec("abc"));
console.log(match[0]);
クラスのabstract修飾子
abstract class C {}
今のところJSDocでabstractを認識させることはできないようです。なお、(TypeScriptが実装していないだけで)JSDoc仕様には @abstract
がありますが、これはabstract method側につけるもののようです。
クラスのimplements節
interface I {
foo: number;
bar(): void;
}
class C implements I {
// ^^^^^^^^^^^^
foo: number = 42;
bar() {}
}
JSDocでは @implements
タグでimplementsを表明することができます。
// interfaceは書けないのでtype aliasで代用
/**
* @typedef I
* @property {number} foo
* @property {function(): void} bar
*/
/** @implements I */
class C {
/** @type {number} */
foo = 42;
bar() {}
}
関数・コンストラクタ呼び出しの型実引数
const arr = new Array<number>();
// ^^^^^^^^
arr.map<string>((x) => `${x}`);
// ^^^^^^^^
JSDocで同等の記述を実現する方法は調べた限りでは無さそうです。他の位置のアノテーションから推論させることで代用できるかもしれません。
// 別の型アノテーションで代用する例
/** @type {number[]} */
const arr = new Array();
arr.map(/** @returns {string} */ (x) => `${x}`);
extendsの型実引数
class Numbers extends Array<number> {}
// ^^^^^^^^
JSDocでは @extends
タグ (別名: @augments
) でextendsを再表明することができます。
/** @extends Array<number> */
class Numbers extends Array {}
タグ付きテンプレートリテラルの型実引数
type Interpolation<P> = (string | ((props: P) => string))[];
function css<P>(texts: TemplateStringsArray, ...interpolation: ((props: P) => string)[]): Interpolation<P> {
const i: Interpolation<P> = [];
texts.forEach((text, index) => {
i.push(text);
if (interpolation[index]) i.push(interpolation[index]);
})
return i;
}
// vvvvvvvvvvvvvvvvvvvvvv
const style = css<{ visible: boolean }>`
color: #335577;
display: ${(p) => p.visible ? "block" : "none"};
`;
JSDocで同等のアノテーションはなさそうです。他の位置のアノテーションで代用できないか考えるといいでしょう。
// 他の位置のアノテーションで代用する例
/**
* @template P
* @typedef {(string | ((props: P) => string))[]} Interpolation
*/
/**
* @template P
* @param {TemplateStringsArray} texts
* @param {...(function(P): string)} interpolation
* @returns {Interpolation<P>}
*/
function css(texts, ...interpolation) {
/** @type {Interpolation<P>} */
const i = [];
texts.forEach((text, index) => {
i.push(text);
if (interpolation[index]) i.push(interpolation[index]);
})
return i;
}
/** @type {Interpolation<{ visible: boolean }>} */
const style = css`
color: #335577;
display: ${(p) => p.visible ? "block" : "none"};
`;
JSXの型実引数
import React from "react";
type ItemListProps<I> = {
items: I[];
renderItem: (item: I) => (React.ReactElement | null);
};
function ItemList<I>(props: ItemListProps<I>): React.ReactElement | null {
return <>{props.items.map(props.renderItem)}</>;
}
const elem = (
<ItemList<number>
// ^^^^^^^^
items={[1, 2, 3]}
renderItem={(item) => <a href={`https://example.com/${item}`}>Item {item}</a>}
/>
);
JSDocで同等のアノテーションはなさそうです。他の位置のアノテーションで代用できないか考えるといいでしょう。
// 他の位置のアノテーションで代用する例
import React from "react";
/**
* @template I
* @typedef ItemListProps
* @property {I[]} items
* @property {function(I): (React.ReactElement | null)} renderItem
*/
/**
* @template I
* @param {ItemListProps<I>} props
* @returns {React.ReactElement | null}
*/
function ItemList(props) {
return <>{props.items.map(props.renderItem)}</>;
}
// ItemList<number> だが、この場合は明示しなくてもちゃんと推論される
const elem = (
<ItemList
items={[1, 2, 3]}
renderItem={(item) => <a href={`https://example.com/${item}`}>Item {item}</a>}
/>
);
関数・メソッドの型仮引数
function id1<T>(x: T) {
// ^^^
return x;
}
const id2 = function<T>(x: T) { return x; };
// ^^^
const id3 = <T>(x: T) => x; // JSX有効時はそのままでは通らない
// ^^^
const obj = {
id<T>(x: T) { return x; }
//^^^
};
class C {
id<T>(x: T) { return x; }
//^^^
}
@template
で型仮引数を宣言できます。
/**
* @template T
* @param {T} x
*/
function id1(x) {
return x;
}
/**
* @template T
* @param {T} x
*/
const id2 = function(x) { return x; };
/**
* @template T
* @param {T} x
*/
const id3 = (x) => x;
const obj = {
/**
* @template T
* @param {T} x
*/
id(x) { return x; }
};
class C {
/**
* @template T
* @param {T} x
*/
id(x) { return x; }
}
クラスの型仮引数
class MyArray<T> extends Array<T> {}
// ^^^
型仮引数のextends制約
class PrimitiveArray<T extends number | string | boolean> extends Array<T> {}
@template
に {}
で括った型を記載すると、それがextens制約になります。
/**
* @template {number | string | boolean} T
* @extends {Array<T>}
*/
class PrimitiveArray extends Array {}
型仮引数のデフォルト
type VFC<P = {}> = (props: P) => null;
TypeScript 4.5からデフォルト値を指定できるようになりました。
/**
* @template [P={}]
* @typedef {function(P): null} VFC
*/
interface宣言
interface I {
new(): number[];
<T>(value: T): T;
foo?: number;
bar?(): void;
readonly [Symbol.toStringTag]: string;
[key: number]: string;
}
interface宣言をJSDocで行うことはできないようです。interface宣言はオブジェクトリテラル型のtype宣言で置き換えられることが多い (declaration mergingが効かないなどの違いがある) ので、そちらで代用することになるでしょう。
type宣言
type T = number | string;
@typedef
で置き換えられます。
/** @typedef {number | string} T */
これだけで十分な汎用性がありますが、別の選択肢として以下があります。
// 単純なオブジェクトリテラル型の場合
/**
* @typedef SimpleObjectType
* @property {number} foo
* @property {string=} bar
*/
// 単純な関数型の場合
/**
* @callback FunctionType
* @param {number} foo
* @param {string=} bar
* @returns {Promise<void>}
*/
また type
宣言が生成される特殊なタグとして @enum
があります。
// type State = string; が生成される
/** @enum {string} */
const State = {
OPENED: "opened",
CLOSED: "closed",
};
なお次に説明するようにJSDoc由来で生成される type
宣言はモジュールレベル宣言の場合は自動的にexportされます。
型のexport
export type T = number;
export type U = number;
export { U as switch }; // キーワードも使える
JSDocの @typedef
/ @callback
で生成される type
は自動的にexportされます。
/** @typedef {number} T */
/** @typedef {number} switch */
export {}; // モジュールとして認識させるためのダミー宣言
// export type T = number;
// type _switch = number;
// export { _switch as switch };
export type宣言
class C {}
export type { C }; // 型定義だけエクスポートされる
JSDocで同じことはできないようです。
型のimport
import { Component, FC } from "react";
// クラス
export let elem: Component | undefined = undefined;
// 型エイリアス
export let comp: FC | undefined = undefined;
JavaScriptでも同様にimport宣言が使えますが、import eliminationが起こりません。型エイリアスのインポートは実行時にエラーになる場合があります。
import { Component, FC } from "react";
// クラス (OK)
/** @type {Component | undefined} */
export let elem = undefined;
// 型エイリアス (NG)
/** @type {FC | undefined} */
export let comp = undefined;
かわりにimport型を使うことができます。 (TypeScriptでも利用可能だが、import宣言のほうが便利なのであまり使わない)
/** @type {import("react").FC | undefined} */
export let comp = undefined;
import type宣言
import type { Component } from "react";
JavaScriptで同じことはできませんが、import型で代用できます。 (説明済みなので省略)
型のre-export
export { FC } from "react";
型のみの場合はJavaScriptで同じことをすることはできませんが、 @typedef
である程度代用できます。
// デフォルト型引数が作れないため、厳密に同じではない
/**
* @template P
* @typedef {import("react").FC<P>} FC
*/
export {};
ところで急に話は変わるんですが、私は技術の「細部」を知ることには「全体像」とは異なる価値があると思っています。この段落を見つけた読者の皆さん、いつも私の記事を細部まで読んでいただきありがとうございます。
re-export形式のexport type宣言
export type { Component } from "react";
本当に型のみをエクスポートしたい場合JavaScriptで同じことをすることはできませんが、 @typedef
である程度代用できます。
// デフォルト型引数が作れないため、厳密に同じではない
/**
* @template P
* @template S
* @typedef {import("react").Component<P, S>} Component
*/
export {};
Legacy import宣言
import React = require("react");
//^^^^
JavaScriptモードではLegacy import宣言を使わなくても require("...")
の形の式が全てインポートと解釈されます。
const React = require("react");
Legacy export宣言
const value = 42;
export = value;
//^^^^^^
JavaScriptモードでは module.exports
に代入することでLegacy export宣言と同等に解釈されます。
const value = 42;
module.exports = value;
export as namespace 宣言
export as namespace
は型定義ファイルでのみ使えます。
// .d.ts, .d.mts, .d.cts でのみ使える
declare namespace AwesomeUMDLibrary {}
export = AwesomeUMDLibrary;
export as namespace AwesomeUMDLibrary;
関数・メソッドのオーバーロード宣言
function multiply(base: string, by: number): string;
function multiply(base: number, by: number): number;
function multiply(base: string | number, by: number): string | number {
if (typeof base === "string") {
let result = "";
for (let i = 0; i < by; i++) result += base;
return result;
} else {
return base * by;
}
}
JSDocによるオーバーロード宣言は実装されていません (関連issue)。
TypeScript 4.4時点ではoverloaded call signaturesで書いてもうまく動かないようです。
// overloaded call signatures による実装 (うまく動かない)
/**
* @type {(function(string, number): string) & (function(number, number): number)}
*/
function multiply(base, by) {
if (typeof base === "string") {
let result = "";
for (let i = 0; i < by; i++) result += base;
return result;
} else {
return base * by;
}
}
function declarationを使うのをやめて @type
によるasキャストを使えばある程度動きます。
const multiply =
/**
* @type {(function(string, number): string) & (function(number, number): number)}
*/
(function(base, by) {
if (typeof base === "string") {
let result = "";
for (let i = 0; i < by; i++) result += base;
return result;
} else {
return base * by;
}
});
他に以下のような機能を組み合わせたワークアラウンドが考えられます。
- union type (引数の型だけ違う場合はこれでOK)
- rest parameters as a tuple (引数の数や組み合わせが違うとき、union typeと組み合わせて使える)
- generics + conditional types (引数型に応じて戻り値型を決める)
メソッドのオーバーロード宣言についても同様です。
アンビエント変数・関数・クラス宣言
declare const x: number;
declare function f(): void;
declare class C {}
JSDocで同等の宣言はできないようです。
アンビエントグローバル宣言
declare global {
var root: HTMLDivElement;
}
export {};
JSDocで同等の宣言はできないようです。
モジュール拡張
declare module "react" {
interface FunctionComponent {
extraField?: number;
}
}
JSDocで同等の宣言はできないようです。
namespace
namespaceはTypeScriptの独自機能ですが、JavaScriptでも特定のパターンはnamespaceと同等に扱われます。
// TypeScript
namespace utils {
export type T = number;
export function square(x: number) {
return x * x;
}
}
// JavaScript
/** @typedef {number} utils.T */
const utils = {};
/** @param {number} x */
utils.square = function(x) {
return x * x;
}
型推論の詳細
デフォルト型引数
TypeScriptでは unknown
にフォールバックされるのに対し、JavaScriptでは any
になります。
const x = new Promise(() => {});
// TypeScript mode: Promise<unknown>
// JavaScript mode: Promise<any>
代入宣言
TypeScriptコンパイラは代入を擬似的な宣言としてみなすことがあります。特定のケースを除き、JavaScriptファイルでのみ有効な機能です。
プロパティ代入宣言
オブジェクト型の変数に代入すると名前空間として推論されます。
const ns1 = {};
ns1.ns2 = {};
ns1.ns2.decl1 = 42;
ns1.ns2["decl2"] = 42;
Object.defineProperty(ns1.ns2, "decl3", { value: 42 });
// declare namespace ns1 {
// namespace ns2 {
// const decl1: number;
// const decl2: number;
// const decl3: number;
// }
// }
関数に対する代入でも名前空間が形成されます。この形だけ特別に、TypeScriptファイル内でも認識されます。
function f() {}
f.foo = 42;
// declare function f(): void;
// declare namespace f {
// const foo: number;
// }
プロトタイプ代入宣言
関数の prototype
属性内への代入はクラスに推論されます。
function C() {}
C.prototype.decl1 = function() {};
Object.defineProperty(C.prototype, "decl2", { value: 42 });
// declare function C(): void;
// declare class C {
// decl1(): void;
// readonly decl2: number;
// }
prototype
属性自体を置き換えることもできます。
function C() {}
C.prototype = {
decl1() {}
};
// declare function C(): void;
// declare class C {
// decl1(): void;
// }
this
代入宣言
コンストラクタ内やメソッド内での this
内プロパティへの代入は型推論に使われることがあります。
class C {
constructor() {
this.foo = 42;
}
}
// declare class C {
// foo: number;
// }
class C {
initialize() {
this.foo = 42;
}
}
// declare class C {
// initialize(): void;
// foo: number | undefined;
// }
CommonJSモジュール内の代入はエクスポートに推論されます。
module.exports.foo = 42;
this.bar = 42;
// export var foo: number;
// export var bar: number | undefined;
グローバルスクリプト内での代入はグローバル変数宣言に推論されます。
this.foo = 42;
// declare var foo: number | undefined;
CommonJSモジュールエクスポート宣言
module.exports
または exports
への代入宣言はモジュールエクスポートとみなされます。
exports.decl1 = 42;
module.exports.decl2 = 42;
Object.defineProperty(exports, "decl3", { value: 42 });
Object.defineProperty(exports, "decl4", { value: 42 });
exports.ns1 = {};
exports.ns1.decl5 = 42;
// export var decl1: number;
// export var decl2: number;
// export var decl3: number;
// export var decl4: number;
// export namespace ns1 {
// const decl5: number;
// }
module.exports
を直接置き換えることもできます。
module.exports = {
decl1() {},
decl2() {},
};
// // TypeScript 4.4時点ではなぜか二重に出力される
// export function decl1(): void;
// export function decl1(): void;
// export function decl2(): void;
// export function decl2(): void;
コンストラクタ関数の型付け
JavaScriptファイルの場合のみ、 function
式や宣言をクラスと同等に扱う仕組みがあります。
// class PackageVersion として扱われる (自動推論)
/**
* @param {string} pkgname
* @param {string} version
*/
export function PackageVersion(pkgname, version) {
this.pkgname = pkgname;
this.version = version;
}
コンストラクタ定義メンバの処理
コンストラクタ内で this
のプロパティに代入すると、推論された型に基づいてクラスフィールドが宣言されたことになります。
class C {
// x: number; // 推論される
constructor() {
this.x = 42;
}
}
なお、TypeScriptでも「noImplicitAny
が有効」かつ「クラスフィールド宣言が存在する」ときに同じ仕組みで型が決められます。
// TypeScriptモード、 noImplicitAnyが有効
class C {
x; // number型に推論される
constructor() {
this.x = 42;
}
}
CommonJS exports / module.exportsの扱い
JavaScriptで書かれたCommonJSモジュールの場合は exports
のプロパティへの代入がエクスポートとして特別に推論されます。
// export var foo: number; に推論される
exports.foo = 42;
// declare const _exports: number; export = _exports; に推論される
module.exports = 42;
CommonJS requireの扱い
JavaScriptで書かれたCommonJSモジュールの場合は require("...")
がLegacy import構文と同等に扱われます。
構文的曖昧性の詳細
関数・コンストラクタ呼び出しの型実引数
関数呼び出しやnewにはジェネリクス引数を指定することができます。
const arr1 = Array<number>(0);
const arr2 = new Array<number>(0);
ところが、これは厳密にはJavaScriptの構文と衝突します。
// JavaScriptとしてパースして、わかりやすさのために括弧をつけ直すとこうなる
const arr1 = (Array < number) > 0;
const arr2 = ((new Array) < number) > 0;
この曖昧性が実際に問題になる例が#36662で挙げられています。このような理由から、JavaScriptでは以下がパースされません。 (TypeScript 4.2以降の仕様)
- 関数呼び出しの型実引数
Array<number>()
- コンストラクタ呼び出しの型実引数
new Array<number>()
- クラスの継承元
class Numbers extends Array<number> {}
- JSXの型実引数
<ItemList<Item> />
- これは曖昧性はなさそうですが、#36673では関数呼び出し・コンストラクタ呼び出しとあわせて禁止するように変更されています。
@type
の使い方一覧
関数・メソッド
関数やメソッドのアノテーションには通常 @param
/ @returns
/ @this
を使いますが、 @type
でも代用できるようになっています。
/** @type {function(number): number} */
function square(x) {
return x * x;
}
Parenthesized expression
丸括弧のうち、構文の優先順位を調整するなどの目的で使われる丸括弧 ((1 + 1) * 2
や (0, foo).bar
など) をParenthesized expression といいます。 Parenthesized expressionに @type
がついていると as
による型キャストと同等の効果が得られます。 (→getContextualType
, checkParenthesizedExpression
)
// (21 + 21) as 42
const x = /** @type {42} */ ( 21 + 21 );
// as と同様、明らかに怪しいキャストはエラーになる
const y = /** @type {number} */ ( "foo" + "bar");
オブジェクトリテラルのメンバー
オブジェクトリテラルの各メンバーに @type
をつけると型アノテーションとして機能します。
const { foo } = {
/** @type {number | string} */
foo: 42,
};
catch(e)
catch
で受け取る変数は any
または unknown
型になりますが、JSDocの場合は @type
で型を指定できます。
// fの戻り値型に注目
function f() {
try {
//
} catch (/** @type any */ e) {
return e;
}
}
function g() {
try {
//
} catch (/** @type unknown */ e) {
return e;
}
}
Assignment declaration
TypeScriptの型検査器では、代入式は特定の条件下で宣言と同等に扱われます。特に左辺がExpando Initializerの場合、そこに @type
をつけることでヒントとすることができます。
const x = {};
/** @type {{ bar?: number }} */
x.foo = {};
Special property declaration
プロパティを読み取るだけの文に @type
をつけると Special property declaration とみなされ、代入先オブジェクトが名前空間として扱われます。
const x = {};
/** @type {number} */
x.foo;
// x["foo"], x[0], x[Symbol.something] 等もOK
JSDoc内の型の構文
JSDocで型が要求される場所 (大抵は {}
で囲まれている) にはTypeScriptの型を書くことができます。
/** @type {number | string} */
let x = 42;
x = "foo";
通常のTypeScriptの型 (関数戻り値に書けるもの) に加えて、JSDocでは以下の構文が追加でパースされます。
JSDoc | TypeScript |
---|---|
* |
any |
? |
any |
?T T?
|
[`T |
!T T!
|
T |
T= |
[`T |
function(T0, T1): T2 |
(arg0: T0, arg1: T1) => T2 |
...T |
T[] |
T.<Arg0, Arg1> |
T<Arg0, Arg1> |
module:T |
any |
String |
string |
Number |
number |
Boolean |
boolean |
Void |
void |
Undefined |
undefined |
Null |
null |
function |
Function |
array |
any[] |
promise |
Promise<any> |
Object<number, T> |
Record<number, T> |
Object<string, T> |
Record<string, T> |
Object |
any |
-
?T
,T?
- 前置と後置に意味的な違いはない。
- strictNullChecksが無効の場合は
?
は無視され、中身T
がそのまま使われる。
-
!T
,T!
- 前置と後置に意味的な違いはない。
- TypeScriptは
!
の意味を特に解釈しない。number!
はnumber
と同じで、number?!
はnumber?
と同じ。
-
function(T0, T1): T2
- 戻り値型は省略可能
- 引数の名前は原則として書かない。例外として
new: T
とthis: T
だけ名前をつけられる。 -
new: T
は第一引数にのみ置ける。あると関数型ではなくコンストラクタ型になり、T
が戻り値型として使われる (構文上の戻り値型は使われなくなる) -
this: T
は第一引数にのみ置ける。通常のTypeScriptのそれと同じ。 -
function
に(
が後続しないときは単独でFunction
型として解釈される。
-
T=
- JSDocタグ直下の型と
function()
の引数位置で使用可能。 -
@param
,@property
の型で使うとオプショナル引数・オプショナルプロパティとして扱われる。 - TypeScript 4.4時点では、
function()
の引数位置やクラスプロパティの型アノテーションで使われた場合はオプショナル引数・オプショナルプロパティとしては扱われない。
- JSDocタグ直下の型と
-
...T
- JSDocタグ直下の型と
function()
の引数位置で使用可能。 - 最後の引数でのみ使用可能。
- 配列型ではなく要素の型を指定する。 (
function(...number): number
は(...args: number[]) => number
と解釈される)
- JSDocタグ直下の型と
-
module:T
- JSDocのnamepath仕様で混乱しないようにパースするようになっているが、その意味はTypeScriptは解釈しない。
- かわりに
any
型として扱われる。
-
array
,promise
,Object
- noImplicitAny が無効のときだけ解釈される。 (結果が
any
を含むため)
- noImplicitAny が無効のときだけ解釈される。 (結果が
-
Object
- 特殊パターンである
Object<number, T>
/Object<string, T>
に該当しない、かつ noImplicitAnyが有効なときは通常通りObject
(object
ではない) として解釈される。
- 特殊パターンである
ただし、いくつかのJSDoc型は直接 .d.ts
にシリアライズしたとき (serializeExistingTypeNode
) と型チェッカーの処理を経由したとき (getTypeFromTypeNode
→ typeToTypeNode
) で挙動に一貫性がありません。TypeScript 4.4時点での挙動差異として以下があります。
-
?
(JSDocUnknownType)- 直接シリアライズすると
unknown
になる。 - 型システム上は
any
として扱われる。
- 直接シリアライズすると
-
...T
(JSDocVariadicType) が関数のrest parameter以外で使われたとき- 直接シリアライズすると
T[]
になる。 - 型システム上は
T | undefined
として扱われる。
- 直接シリアライズすると
-
?T
/T?
(JSDocNullableType)- 直接シリアライズすると
T | null
になる。 - 型システム上も
T | null
として扱われるが、--strictNullChecks
がオフのときはT
として扱われる。
- 直接シリアライズすると
-
T=
(JSDocOptionalType)- 直接シリアライズすると
T | undefined
になる。 - 型システム上も
T | undefined
として扱われるが、--strictNullChecks
がオフのときはT
として扱われる。
- 直接シリアライズすると
-
module:T
(JSDocNamepathType)- 直接シリアライズすると
any
になる。 - 型システム上は
any
の亜種である errorType として扱われる。
- 直接シリアライズすると
更新履歴
- 2021-11-06 公開。
- 2021-11-20 TypeScript 4.5 の変更点を反映。
Discussion