🧊

JSDoc TSでジェネリクスを使う

あんまり知られてないかもしれんけど、JSDocのコメントを書くことでもTypeScriptの恩恵は受けられる。

より正確には、

  • JSDocコメントを書くことで
  • `.js`ファイルであっても
  • TypeScriptのLanguage Serverの機能を(一部)利用できる

というわけ。

で、これが結構便利で最近はよく使ってるのです。

ただ、いわゆるジェネリクスを扱いたいときに困ったことがあったので、それについてのメモを。

ジェネリクスは不完全なサポート

たとえば、コレは動作する。

/**
 * @template T
 * @param {T} x
 * @return {T}
 */
function id(x) {
  return x;
}

const a = id("string");
const b = id(123);
const c = id({});

`@template`で予め名前を宣言しておけば、こんな風に関数をジェネリクスで定義することはできる。
TSで書くと、`function id(x: T): T { return x; };`相当のもの。

けど、それ以外のジェネリクスに関することができなくて、単に型だけを定義することもできない。

type Result<Resp> = { ok: true, data: Resp };

こういう型は定義できないし、クラスでも同様に使えない。

どうやらこれは現状の仕様らしい。

Allow to explicitly pass type parameters via JSDoc · Issue #27387 · microsoft/TypeScript · GitHub

困った例

具体的な例でいうと、Reactの`useState()`を使うときに、その値が推論できない場合。

// 推論できる
const [count, setCount] = useState(0);

// 推論できない
const [items, setItems] = useState([]); // Item[]
const [error, setError] = useState(null); // or Error

TSだったならばこう書けるところ、JSDoc TSではそれができない。

const [items, setItems] = useState<Item[]>([]);
const [error, setError] = useState<Error | null>();

これを解決するための方法は現状ないらしく、ワークアラウンドとしてはこうするしかない。

/** @type {Item[]} */
const initItems = [];
const [items, setItems] = useState(initItems);

// or

const [error, setError] = useState(
  /** @type {Error | null} */ (null)
);

というように、

  • 別の所で推論できるようにした変数を渡す
  • キャストする

このどっちかしかないらしい。

それでも便利なので

訳あってTSが使えない・使いたくないときに、局所的にでも手堅く書ける選択肢があるというのは嬉しいこと。
型の補完ができるのであれば、設定ファイルをわざわざTSで書く必要もないし、必要十分ってわけで。

これだとJSのまま書けるので、TSにコンパイルされたときのオーバーヘッドもないので、コードを小さく保ちたいライブラリを作る時とかにも便利。
(いまどきどれだけの人がコンパイル後のコードを意識しながらコードを書いてるのやらって感じではあるけど)
(そういえば昔Flowっていうのがあったな・・元気かな・・)

いきなりTSはちょっと・・っていう初学者にもとてもいいと思うし、割とオススメかなと思ってる今日この頃。

まぁ最初からTSでやればよかった・・ってなったら、その時は素直にTSにしましょう!