TypeScriptでReactとBFFの間の通信にカタをつける

これは TECHSCORE Advent Calendar 2019 の1日目の記事です。

TL;DR

TypeScript で React と BFF の間の通信に型をつけてみたよ。
インターフェース定義は TypeScript で書いてるので、実装がそのままドキュメントになるよ。
React 側も BFF 側もきっちり型がつくので開発捗るよ。
ここに実装置いたから見てね。

はじめに

最近半年ほど TypeScript ばっかり書いてます。TypeScript で React App 書いて、TypeScript で BFF 書いて、「嗚呼、型があるって幸せだな」って思ってました。
でも唯一不満があります。React と BFF の間の通信。 fetchを使って書いてるんですが、 fetchで JSON 取ってくると戻り値は anyになっちゃうんですよね、当たり前ですけど。

これなんとかしたいな、 fetch使った通信でもちゃんと型つけたいな。
こういうときは Swagger かな、でも Swagger 面倒だな、文法覚えらんないし。
そもそも React も BFF も TypeScript で書いてるのに、わざわざ別の技術持ち込むのも嫌だな。

そういう訳で考えてみました。

方針

  1. React と BFF の間のインターフェースは TypeScript で記述する。つまり IDL = TypeScritpt。
  2. 関数呼び出しっぽく書けるようにする。
  3. クライアント側の実装はなるべく共通化する。
  4. サーバー側の実装はなるべく共通化する。

昔懐かしい CORBA の IDL とか、java.rmi のイメージです。


Stub と Skeleton、Express App は自動生成するのが王道ですが、まぁそれは別の機会に。
以下、図中の丸数字の順に作っていきます。

① インターフェース定義

ユーザーを操作するリモートインターフェースを考えます。
このインターフェースは createreadなどの関数を持っていて、それぞれの引数にどんな型を取りうるか、戻り値がどんな型なのか、ということを素直に TypeScript で記述するとこんな感じになります。

各関数はオブジェクト型の引数を1つだけ取るようにしています。
・引数として複数の値を渡したい時はオブジェクトのプロパティとして渡してね。
・引数はJSON化されて fetchでリクエストボディとして送信されるよ。
fetchのレスポンスが関数の戻り値として返ってくるよ。
という想定です。

② Stub

Stub はクライアント側の処理で、実際の通信を担う部分です。
要は fetchを実行して、そのレスポンス(JSON)に型をつけたり、リクエストを型で制限できるようにします。

と、その前に、いくつか便利な定義をしておきます。
リモートインターフェース内で定義するリモート関数(上記の createreadなど)の型を RemoteFunctionとし、 RemoteParameterでリモート関数の引数の型を取得できるようにします。

さて、Stub(=実際に fetchするところ)を作ります。

postは「リモート関数と URL を指定すると、 fetchしてくれる非同期関数を返す」関数です。
Client で「 fetchしてくれる非同期関数」を呼び出すという想定です。

post関数は以下のように使います。
これによって URL とリモート関数の型を結びつけています。

VS Code で見ると、 createの引数と戻り値にちゃんと型がついていることがわかります。

ジェネリクスで型を指定しないと anyになってしまいます。これは嬉しくない。

さらにリモート関数をひとまとめにしておきます。

AsyncRemoteの定義はこんな感じ。インターフェース定義の各関数を非同期にしたもの、を作成するのに使います。

AsyncRemoteを使うことで、 UserBffのプロパティに過不足がある場合にコンパイルエラーになってくれます。とても嬉しい。

③ Client

UserBff.createは通常の関数呼び出しのように使います。 とても簡単。

④ ServerImpl(サーバー実装)

次にサーバー側でリモートインターフェースを実装します。
UserBffType['create']によって、引数と戻り値の型を指定しています。このおかげで、引数のプロパティを間違えたり、戻り値のプロパティに不足があったりした場合には、コンパイルエラーになります。これも嬉しい。

⑤ Skeleton

サーバー側でリクエストを受け取って、サーバー実装を呼び出す箇所を作ります。
フレームワークによって実装方法が違いますが、以下は Express の例です。

「ServerImpl で実装した関数を Express のルーティングコールバックに変換する関数」を作っています。
ルーティングコールバック内では、Express のリクエストからボディを取り出して funcに渡し、戻り値をレスポンスとして返します。

⑥ Express App

requestHandlerはこんな風に使います。
これによって URL とリモート関数の実装を結びつけています。

 
以上、すべての実装はこちらに置いてます。

章ごとの実装はこちら↓
① インターフェース定義
② Stub
③ Client
④ ServerImpl(サーバー実装)
⑤ Skeleton
⑥ Express App

記事中で触れなかった「サーバー側にエンドポイントがなかった場合はどうなるの?」とか「サーバー側で例外が発生した場合はどうなるの?」に関しても実装しています。

さいごに

TypeScript で React と BFF の間のインターフェースを定義してみました。
ジェネリクスでごにょごにょすることで、ユーザーがクライント側で呼び出す関数と、サーバー側で実装する関数に型をつけることができました。
インターフェース定義がそのままドキュメントになるし、型の不一致はコンパイルエラーになるしで、いいことづくめです。
最初に「Stub と Skeleton、Express App は自動生成するのが王道」と書きましたが、このくらいの量なら手で書いても大丈夫かな、型がないよりある方がいいしね。

なお、今回は React と BFF の間の通信に特化した内容でしたが、同じ手法で既存の Web API を呼び出す場合(つまり BFF と API の間の通信)に TypeScript で型をつけることもできます。これについてはまたの機会にでも。

Comments are closed, but you can leave a trackback: Trackback URL.