📐

ClusterScriptでも型チェック&コード補完

2024/12/10に公開

この記事は「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です。
このLabelsstringなので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の宣言を書き換えました。
prefixLabelsのみに制限しています。

するとどうでしょう!!あちこちでエラーが発生しはじめました!!

型チェックが効いてますね、最高です。

ちなみにこっちではエラーになってません。

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を指定します。そしてそのTprefixの型に指定します。
つまり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()を実行する時に、どの型を使うかを指定すればいいわけです。
しかしこのコードにはバグがあります。


あっ、コピペで増やしたのバレた。
enumTagTagsなので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