JavaScriptの型定義の方法

プログラミング
スポンサーリンク

はじめに

こんにちは!

現在、昔のコードを見直しているのですが、どのように型を定義したらいいか迷って、そのたびに調べたりしていました。型定義の書き方については、もうだいぶ昔に以下のような記事を書いたことがあるのですが

今回は JSDoc 向けに絞って、もっといろいろな書き方を紹介します。

型定義テクニック

JavaScriptは動的型付け言語ですが、VSCodeの『jsconfig』と『JSDoc』を組み合わせることで型情報を付加し、開発を円滑に進めることができます。

設定ファイル

最初に私の jsconfig.json を紹介します。checkJstrueにして型推論させます。

{
	"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を許容することになります。

複数の型が入る変数の宣言

Float32ArrayInt32Arrayどちらかが入る型が入る変数の定義です。

/** @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 - 他のファイルに記載してある型情報

おわりに

自由度があってしっかり型情報がかけて驚きですね。

最後まで見ていただきありがとうございました。

コメント

タイトルとURLをコピーしました