TypeScriptで型定義が足りないライブラリを使う|declare moduleで.d.tsを自作する

TypeScriptで外部ライブラリを使っていると、JavaScriptでは問題なく動くのに、型情報が足りずエラーになることがある。
今回の麻雀ゲーム制作でも、外部の麻雀ライブラリを使う中で必要な型情報を自分で補うことになった。
そこで使ったのが、.d.tsファイルとdeclare moduleだ。

.d.tsファイルとは

.d.tsは、TypeScriptに「このライブラリにはこういう型があります」と伝えるための型宣言ファイル。
JavaScriptの処理そのものを書くのではなく、

  • どんな関数があるか
  • どんな引数を受け取るか
  • 何を返すか
  • どんなプロパティを持つか

といった型情報だけを書く。
例えば、外部ライブラリからAIプレイヤーを生成したい場合、簡略化すると次のような型を定義できる。

declare module "@example/mahjong-ai" {
  export type AiPlayer = {
    action: (
      message: Record<string, unknown>,
      callback?: (
        reply?: Record<string, unknown>
      ) => void
    ) => void;

    readonly model: object;
  };

  const Player: new () => AiPlayer;

  export default Player;
}

これでTypeScript側も、

const player = new Player();
player.action(...);

といったコードの型を認識できるようになる。

declare moduleで外部モジュールの型を補う

ポイントになるのが、

declare module "@example/mahjong-ai"

の部分。
ここには実際にimportするときのモジュール名を書く。
例えば、

import Player from "@example/mahjong-ai";

と使うなら、同じ名前でdeclare moduleを定義する。
TypeScriptはこの宣言を見て、外部ライブラリの型情報として扱ってくれる。

最初から全部の型を書く必要はない

今回やってみて分かりやすかったのが、ライブラリ全体の型を最初から再現する必要はないということ。
最初は、

  • AIプレイヤーを生成する
  • action()を呼ぶ
  • modelを見る

程度しか使わないなら、その部分だけ定義すればいい。
その後、新しく牌山を使うなら牌山の型を追加し、対局クラスを使うならその型を追加する。

必要になった機能
↓
必要な型だけ追加
↓
アプリで使う

という形で少しずつ増やしていける。
実際、麻雀ゲームでも使用する機能が増えるにつれて、牌山、手牌、プレイヤー、対局などの型を追加していった。

Record<string, unknown>で逃げ道を作る

外部ライブラリのデータ構造を完全には把握していない段階では、

Record<string, unknown>

もよく使った。
例えば、

type Message =
  Record<string, unknown>;

としておけば、「文字列のキーを持つオブジェクト」であることだけは保証しつつ、細かい構造までは決めなくて済む。
anyでも動くが、unknownにしておけば値を使うときに確認が必要になるため、型チェックを完全に捨てずに済む。
ただし、ずっとRecord<string, unknown>のままにするのではなく、構造が分かってきたものは専用の型に置き換えたほうが扱いやすい。

実際に使うデータから型を細かくしていく

例えば対局結果を扱う段階では、単なるオブジェクトではなく、

type WinResult = {
  points: number;
  fu?: number;
  han?: number;
};

のように必要な値を明示したほうがいい。
こうすると、

result.points

が数値であることをTypeScriptが保証してくれる。
エディタの補完も効くので、外部ライブラリを直接JavaScriptとして扱うよりかなり楽になる。

型定義は実装とは別物

注意したいのは、.d.tsを書いてもライブラリ本体の動きが変わるわけではないこと。
例えば、

type Player = {
  foo: () => void;
};

と勝手に書けばTypeScript上ではfoo()が存在することになる。
しかし、実際のJavaScriptにfoo()がなければ実行時にはエラーになる。
つまり.d.tsは、
実際のライブラリの仕様をTypeScriptへ教えるもの
であって、機能を追加するものではない。
そのため、実際の動作と型定義が一致していることが重要になる。

今回使ってみて感じたこと

型定義が用意されていない、あるいは足りないライブラリを見ると、最初はTypeScriptでは使いにくそうに見える。
ただ、.d.tsとdeclare moduleを使えば必要な部分だけ型を補える。
特に今回のように、

  • JavaScript向けライブラリを使いたい
  • TypeScriptから安全に呼び出したい
  • ライブラリ全体の型を書く必要はない

という場合にはかなり便利だった。
最初は最小限の型だけ用意して、使う機能が増えたら少しずつ型を追加する。
この方法なら、型定義が十分でないライブラリでもTypeScriptの恩恵を受けながら利用できる。

コメント

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