ClusterScriptでも型チェック&コード補完
この記事は「Cluster Script Advent Calendar 2024」の10日目です
昨日はやまちゃんさんの「複数ワールドに拡張しやすい外部通信機能のエンドポイント設計 Python/CGI編」でした!
データ構造の大事さがとても良く分かる…!!!
こんばんは!かおもと申します!!!!
前回書いた「ClusterScript+MVVMで作るマインスイーパー」ではJSDocを多用しました。
これは便利そうという話を聞きましたので、その時使用したJSDocをつらつらゆるく紹介していきます。
この辺丁寧に書くと、型チェックとかコード補完してくれるので、だいぶ楽です!
あと、VSCodeで書く前提です。
VSCodeに型チェックをさせる
//@ts-check
VSCode限定と聞いていますが、上の方にこれを追記しましょう。
型チェックとか色々してくれます。
型を定義する
/**
* 番号付きのラベルを作るオブジェクト。
* @typedef {Object} NumberedLabel
* @property {string} prefix
* @property {(step: number) => string} nextLabel step分を追加したラベルを返す。
*/
prefixというプロパティと、nextLabelという関数を持つオブジェクトの型を宣言しています。
JavaScript(ClusterScript)の場合、大抵のものはObjectなので@typedef {Object}
でいけます。
定義した型を返す関数を定義する(ややこしい)
/**
* NumberedLabelを作る。
* @param {number} initial
* @returns {NumberLabel}
*/
const CreateNumberedLabel = (initial) => {
let value = initial;
let prefix = "";
return Object.freeze({
get prefix(){ return prefix }, //プロパティprefixのgetterです
set prefix(v){ prefix = v }, //こっちはsetter
nextLabel(step){
value += step;
return `${prefix}${value}`;
},
});
};
モジュールパータンというやつです。NumberLabel
型のオブジェクトを作って返します。
普通の関数なら@returns {number}
などにすればOKです。
余談ですが、classみたいな使い方ができるので、筆者はこれが大好きです。
では早速効果を確認してみます。
いいですね!エラーになっています!!!
「NumberedLabelを作る。」という説明文も表示されていて、とても良きです。
ちなみにCreateNumberedLabel
の宣言をしていないとこうなります。
initialもanyになっていてダメです…!
Objectをenumっぽく使う
/**
* @typedef {Object} Labels
* @property {string} social
* @property {string} game
* @property {string} art
*
* @enum {Labels}
*/
const Labels = Object.freeze({
social: "social",
game: "game",
art: "art",
});
先程の例のように、@typedef
で定義して、@enum
で指定すればOKです。
このLabels
はstring
なのでNumberLabel.prefix
に指定することができます。
const enumLabel = CreateNumberedLabel(2);
enumLabel.prefix = Labels.game;
$.log(enumLabel.nextLabel(3)) //"game5"が表示される。
この段階では、enumの型チェックとかコード補完の恩恵は、まださほどありません。
次のでだいぶ恩恵受けられます。
そのenumだけ指定したい
/**
* 番号付きのラベルを作る。
* @typedef {Object} NumberedLabel
* @property {keyof Labels} prefix
* @property {(step: number) => string} nextLabel step分を追加したラベルを返す。
*/
先のNumberedLabel
の宣言を書き換えました。
prefix
をLabels
のみに制限しています。
するとどうでしょう!!あちこちでエラーが発生しはじめました!!
型チェックが効いてますね、最高です。
ちなみにこっちではエラーになってません。
Labelsから指定されていると認識できていますね、最高です。
エラーの内容を確認してみます。
言ってる意味が分かりづらいけど、エラーになっているだけでヨシ!
まずは上の方でエラーになってるCreateNumberedLabel
の方から直します。
そこのエラーはreturn prefix;
の型が合ってないってことなので、prefix
の定義から直します。
/** @type {keyof Labels} */
let prefix = "";
prefix
はただのstring
だったので、それをkeyof Labels
にします。
するとエラーの場所が変わります。
順調に直っているな!
そしてエラー内容はこうです。
さっき見たエラー再び。
これ具体的にはLabelsで定義されていないって意味です。
なので初期値としてもLabelsに定義してるものを入れましょう。
ここでコード補完が効きます!
Labelsに定義されてるのが表示されてます、最高だね。
ちなみに宣言がない場合はこうです。
不親切にも程がある。
あの補完に従ってもいいのだけど、筆者はなんだか落ち着かないのでこうしてます。
/** @type {keyof Labels} */
let prefix = Labels.social;
同じようにもう一つのエラーの方も直します。
const numberedLabel = CreateNumberedLabel(1);
numberedLabel.prefix = Labels.art;
$.log(numberedLabel.nextLabel(4)); //"art5"が表示される。
このエラーを直す時Labels
の定義の下にないとエラーになるので、エラーになったら下の方に移動させて下さい。
/**
* @typedef {Object} Labels
* @property {string} social
* @property {string} game
* @property {string} art
*
* @enum {Labels}
*/
const Labels = Object.freeze({
social: "social",
game: "game",
art: "art",
});
const numberedLabel = CreateNumberedLabel(1);
numberedLabel.prefix = Labels.art;
$.log(numberedLabel.nextLabel(4)); //"art5"が表示される。
任意のenumとか指定できるようにする(ジェネリクス)
NumberedLabel
を色々なenumで使えるようにしたいですよね????
こうします!!!
/**
* 番号付きのラベルを作る。
* @template T
* @typedef {Object} NumberedLabel
* @property {T} prefix
* @property {(step: number) => string} nextLabel step分を追加したラベルを返す。
*/
いわゆるジェネリクスです。
@template T
で型パラメータT
を指定します。そしてそのT
をprefix
の型に指定します。
つまりT
に色々なenumとか入れられるというイメージです。
逆にT
を指定する必要があるということで、NumberLabel
を型で指定してるとこでエラーになります。
こういうふうに連鎖的にエラーが出るの助かる。
こういう場合は
* @returns {NumberedLabel<keyof Labels>}
基本的にこういう感じで型を指定しますが、ここの場合は型を指定する意味はあまりないのでany
でよいです(ここの場合はstring
でも)。
ジェネリクスは基本的にCreateNumberedLabel()
を実行するたびにどの型を使うかを指定するものだからです。
なので下のようにすればOKです。あとprefix
初期化のところもLabels
を使わないようにしておきます。
/**
* NumberLabelを作る。
* @param {number} initial
* @returns {NumberedLabel<any>}
*/
const CreateNumberedLabel = (initial) => {
let value = initial;
let prefix = null; //anyなのでnullにしておく
return Object.freeze({
get prefix(){ return prefix }, //プロパティprefixのgetterです
set prefix(v){ prefix = v }, //こっちはsetter
nextLabel(step){
value += step;
return `${prefix}${value}`;
},
});
};
ということで、型を指定するとこはここです。
/** @type {NumberedLabel<keyof Labels>} */
const numberedLabel = CreateNumberedLabel(1);
numberedLabel.prefix = Labels.art;
$.log(numberedLabel.nextLabel(4)); //"art5"が表示される。
/** @type {NumberedLabel<keyof Labels>} */
const enumLabel = CreateNumberedLabel(2);
enumLabel.prefix = Labels.game;
$.log(enumLabel.nextLabel(3)) //"game5"が表示される。
CreateNumberedLabel()
を実行するタイミングで指定します。
試しに別のenumを定義して指定してみます。
/**
* @typedef {Object} Tags
* @property {string} song
* @property {string} dj
*
* @enum {Tags}
*/
const Tags = Object.freeze({
song: "song",
dj: "dj",
});
/** @type {NumberedLabel<keyof Tags>} */
const enumTag = CreateNumberedLabel(3);
enumTag.prefix = Labels.game;
$.log(enumTag.nextLabel(1));
CreateNumberedLabel()
を実行する時に、どの型を使うかを指定すればいいわけです。
しかしこのコードにはバグがあります。
あっ、コピペで増やしたのバレた。
enumTag
はTags
なのでLabels
は指定できないとエラーしてくれてます。
安心してコピペできる。
というわけで、こう修正して終わりです。
/** @type {NumberedLabel<keyof Tags>} */
const enumTag = CreateNumberedLabel(3);
enumTag.prefix = Tags.song;
$.log(enumTag.nextLabel(1)); //"song4"が表示される。
いったんここまでのコード
いったんここまでのコードをまとめます。
色々変えてみたりご自由にどうぞ!!!
ここまでのコード
//@ts-check
/**
* 番号付きのラベルを作る。
* @template T
* @typedef {Object} NumberedLabel
* @property {T} prefix
* @property {(step: number) => string} nextLabel step分を追加したラベルを返す。
*/
/**
* NumberLabelを作る。
* @param {number} initial
* @returns {NumberedLabel<any>}
*/
const CreateNumberedLabel = (initial) => {
let value = initial;
let prefix = null; //anyなのでnullにしておく
return Object.freeze({
get prefix(){ return prefix }, //プロパティprefixのgetterです
set prefix(v){ prefix = v }, //こっちはsetter
nextLabel(step){
value += step;
return `${prefix}${value}`;
},
});
};
/**
* @typedef {Object} Labels
* @property {string} social
* @property {string} game
* @property {string} art
*
* @enum {Labels}
*/
const Labels = Object.freeze({
social: "social",
game: "game",
art: "art",
});
/** @type {NumberedLabel<keyof Labels>} */
const numberedLabel = CreateNumberedLabel(1);
numberedLabel.prefix = Labels.art;
$.log(numberedLabel.nextLabel(4)); //"art5"が表示される。
/** @type {NumberedLabel<keyof Labels>} */
const enumLabel = CreateNumberedLabel(2);
enumLabel.prefix = Labels.game;
$.log(enumLabel.nextLabel(3)) //"game5"が表示される。
/**
* @typedef {Object} Tags
* @property {string} song
* @property {string} dj
*
* @enum {Tags}
*/
const Tags = Object.freeze({
song: "song",
dj: "dj",
});
/** @type {NumberedLabel<keyof Tags>} */
const enumTag = CreateNumberedLabel(3);
enumTag.prefix = Tags.song;
$.log(enumTag.nextLabel(1)); //"song4"が表示される。
その他色々
個人的なメモレベルでダラダラと増やしていきます。
Promiseでresolveの型を指定する
JintではExperimentalになっていたけど、ClusterScript(2.30.0)では使えるようなのでメモ。
//@ts-check
/** @type {Promise<string>} */
let p = new Promise((resolve) => {
$.log("start");
resolve("ok"); //string以外を入れるとチェックされる
});
p.then((v) => {
$.log(v); //vはstringと認識されている
});
他にもあったら増やします
よきClusterScriptライフを!!!
明日の記事は獏星さんの「アバターの姿勢操作まわりの何か」です!!
姿勢制御まわりのAPIも中々に生っぽくて大変なので楽しみです!!!
Discussion