はじめに
こんにちは!
現在、昔のコードを見直しているのですが、どのように型を定義したらいいか迷って、そのたびに調べたりしていました。型定義の書き方については、もうだいぶ昔に以下のような記事を書いたことがあるのですが
今回は JSDoc 向けに絞って、もっといろいろな書き方を紹介します。
型定義テクニック
JavaScriptは動的型付け言語ですが、VSCodeの『jsconfig
』と『JSDoc
』を組み合わせることで型情報を付加し、開発を円滑に進めることができます。
設定ファイル
最初に私の jsconfig.json
を紹介します。checkJs
をtrue
にして型推論させます。
{ "buildOnSave": false, // 保存時にビルドしない "compileOnSave": false, // 保存時にコンパイルしない // コンパイルオプション "compilerOptions": { "ignoreDeprecations": "5.0", "newLine": "LF", // 生成コードの改行コードを指定する "lib": ["es2015", "dom"], // コンパイルに含めるライブラリの指定 "module": "es2015", // モジュールの方式 "target": "es2015", // 出力する ECMAScript のバージョン "checkJs": true, // JavaScript ファイルの型整合性などをチェック "noImplicitAny": true, // 暗黙的な any をエラーにする "noEmit": true, // emit を作成しない "moduleResolution": "Node" }, "exclude": [ // 除外ファイル "node_modules" ] }
基本的な型の明示
JavaScriptでは変数の型が明示されないため、変数の内容が途中で変更されてしまう可能性があります。
const result = getValue();
型を明示することで問題を防げます。
/** * @type {number} */ const result = getValue();
途中で値を受け取る際は、以下のよう記載することで一時的に型を指定できます。
const result = /** @type {number} */ (getValue());
変数を動的に扱うときのインデックスエラー
以下のようなコード
const params = { a: 1, b: 2, c: 3 }; for (const key in params) { params[key] *= 2; // エラーが出る }
次のようなエラーが発生します。
型 'string' の式を使用して型 '{ a: number; b: number; c: number; }' にインデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。 型 'string' のパラメーターを持つインデックス シグネチャが型 '{ a: number; b: number; c: number; }' に見つかりませんでした。
@typedef {"a" | "b" | "c"}
とすることで特定の文字列のみ許可する型となり、エラーは発生しません。
/** @typedef {"a" | "b" | "c"} ParamKey */ for (const key in params) { const typedKey = /** @type {ParamKey} */ (key); params[typedKey] *= 2; }
既存クラスにプロパティを追加させる
例えば、String
クラスに明示的に extra
というプロパティを持ったの定義方法です。
/** @typedef {String & { extra: any }} ExtendedString */ /** @type {ExtendedString} */ const greeting = Object.assign(new String("Hello"), { extra: 123 });
{型A & 型B}
と記載すると複数の型を結合するIntersection型
を表します。これにより、既存の型と追加したいプロパティの両方を持つ型になります。
省略可能な引数を持つ関数の定義
引数が1つまたは2つの場合がある関数の定義方法です。
/** @type {function(number, string=): void} */ const myFunction = function(num, str) { console.log(num, str); };
{型=}
と書くことで引数をオプショナルに設定できます。
ネストしたオブジェクトの型指定
プロパティ hoge
が必ず存在するが、その中身のプロパティの必要性は任意というパターンの定義です。
/** @typedef {{ [key: string]: number }} StringNumberMap */ /** @typedef {{ hoge: StringNumberMap }} HogeObject */ /** @type {HogeObject} */ const obj = { hoge: { anyKey: 10 } };
型定義を分けることで、ネストした構造でも正しく型推論を作ります。また、{[key: string]: number}
は自由なキーを持つオブジェクトで、その値は数値であることを示します。
nullを許可するプロパティ
文字列またはnull
が格納されるプロパティの定義です。
/** @typedef {{ test: ?string }} NullableString */ /** @type {NullableString} */ const data = { test: null };
{?型}
はnullable型
を表し、その型またはnull
を許容することになります。
複数の型が入る変数の宣言
Float32Array
かInt32Array
どちらかが入る型が入る変数の定義です。
/** @type {(typeof Float32Array | typeof Int32Array)} */ const TypedArrayClass = Float32Array;
typeof
を付けることで型を表すようになり、また |
を使うことで、どちらか一方の型を許可できます。
複数引数関数の一部だけ型指定
第1引数は数値、第2引数以降は自由といった関数の定義方法です。
/** @type {function(number, *): void} */ const func = function(first, ...args) { console.log(first, args); };
/** * @type {function(number, *):void} */ const callback = (num, ...) => {};
任意型 *
を使うことで、最初の引数だけを固定できます。
型情報だけを別ファイルにまとめる
typedefs.js
に型をまとめ、別ファイルでimport
します。
// typedefs.js /** * @typedef {Object} TypeA */ /** * @typedef {Object} TypeB */ export default {};
型情報を使用したいソースコードの上部にて以下を記載します。
// 使用側ファイル /** @typedef {import('./typedefs.js').TypeA} TypeA */ /** @typedef {import('./typedefs.js').TypeB} TypeB */
このように import
を利用して再利用可能な型定義を行えます。
数値をキーに持つ連想配列
以下のように指定することで数値をキーとして明確にできます。
/** * @type {Object<number, string>} */ const numMap = {}; numMap[1] = "value";
コールバック関数を明示する
@typedef
で1行で関数型も書けますが、@callback
を使用して複数行に渡って定義することもできます。
/** * @callback CallbackFunction * @param {string} input * @returns {void} */ /** @type {CallbackFunction} */ const cb = (input) => {};
このように、@callback
は関数の引数と戻り値を分けて明示的に書ける、ドキュメント生成時にも「コールバック関数」として明示されるといったメリットがあります。
任意のプロパティが混在
[B]
のように[]
で囲むととプロパティが任意になります。
/** * @typedef {Object} OptionalData * @property {number} A * @property {number} [B] */
これにより、必ず存在するプロパティA
、任意のプロパティB
といった定義も可能になります。
その他
前回の記事の内容で、今回の記事の中には載っていなかったものもあるので、ざっと紹介。
関数の定義
関数の場合は、以下のようになります。
/**
* ~する関数
* @param {number} x - 引数1
* @param {string} y - 引数2
* @returns {string}
*/
static myfunc(x, y) {
return "test";
}
paramの記載方法
型は次のように書くことが出来ます。
@param {number|string} x - 数値か文字列を引数とする。
@param {Array} x - 配列を引数とする。
@param {?Object} x - オブジェクトを引数とするが、nullを指定してもよい
@param {!Object} x - オブジェクトを引数とする。nullを指定してはいけない
@param {?(number|string)} x - 数値か文字列あるいは、nullを指定しても良い。
@param {number} [x] - 数値を引数とするが、省略してもよい。
@param {number} [x=10] - 数値を引数とするが、省略してもよい。省略した場合はデフォルトを10とする。
@param {{A: number, B: string}} x - AとBをメンバに持つオブジェクトを引数とする。
@param {number[]} x - 数値型の配列。
@param {Object<string, number>} x - キーを文字列、値を数値としたハッシュオブジェクト。
@param {...number} x - 数値型の可変引数
@param {typeof ClassX} x - ClassXの型を引数とする。
@param {function(number, string): boolean} func - 第1引数には数値、第2引数は文字列、戻り値はboolの関数
@param {import("./aaa/bbb.js").MyType} x - 他のファイルに記載してある型情報
おわりに
自由度があってしっかり型情報がかけて驚きですね。
最後まで見ていただきありがとうございました。
コメント