きみはねこみたいなにゃんにゃんなまほう

ねこもスクリプトをかくなり

TypeScript の型定義を観点に gRPC-Web の実装ライブラリを比較する

gRPC-Web の実装を調べてみると、どうやら複数の実装があるみたいです。 それぞれ関連ライブラリの組み合わせが決まっており、きちんと区別しないと混乱しそうなのでまとめてみます。

比較のために作成したプロジェクトは以下に置いてあります。

gRPC-Web の実装を簡易比較

  • grpc/grpc-web (star: 700)
    • grpc のオーガニゼーション(以下本家)にある...と思ってこれを選ぶとまだ成熟しておらず後悔する
    • 特に TypeScript の型定義は完全にサポートされていない
      • アクセッサの戻り型が {} で潰されていたりする
  • improbable-eng/grpc-web (star: 1.7k)
    • 本家よりも勢いがあって利用者も多そう
    • TypeScript の型定義は本家よりも充実している

なので gRPC の本家本元のリポジトリにも grpc/grpc-web という実装はあるけれど、 improbable-eng/grpc-web の方が人気があり実装状況もマルという状況ですね。

以下、両者を細かく見ていきます。 比較の題材にした Protobuf 定義は以下の通りです。

syntax = "proto3";

package hello;

service Greeting {
  rpc Hello(HelloRequest) returns (HelloResponse) {}
}

message HelloRequest {
  string message = 1;
}

message HelloResponse {
  string message = 1;
  Profile profile = 2;
}

message Profile {
  string name = 1;
}

grpc/grpc-web

本家お膝元のリポジトリです。 しかしもう一つのリポジトリと比較すると実装に開きがあります。

protoc \
  --js_out=import_style=commonjs:${OUT_DIR} \
  --grpc-web_out=import_style=commonjs,mode=grpcwebtext:${OUT_DIR} \
  hello.proto

のように生成します。 これで以下のようなファイルが生成されます。

out/grpc-web-by-grpc/proto
├── hello_grpc_web_pb.d.ts
├── hello_grpc_web_pb.js
├── hello_pb.d.ts
└── hello_pb.js

上で書いた「TypeScript の型が {} で潰される」とは以下のように Profile 型が返ってきてほしいところが {} になっているところを指しています。

export class HelloResponse {
  constructor ();
  getMessage(): string;
  setMessage(a: string): void;
  getProfile(): {};
  setProfile(a: {}): void;
  serializeBinary(): Uint8Array;
  static deserializeBinary: (bytes: {}) => HelloResponse;
}

おそらくまだ実装途中なのだと思います。 toObject の型が定義されていないのも物足りないところですね。

improbable-eng/grpc-web

本家よりも勢いのあるリポジトリです。

protoc \
    --js_out="import_style=commonjs,binary:${OUT_DIR}" \
    --ts_out="service=true:${OUT_DIR}" \
    hello.proto

のように生成します。これで以下のようなファイルが生成されます。

out/grpc-web-by-improbable-eng/proto
├── hello_pb.d.ts
├── hello_pb.js
├── hello_pb_service.d.ts
└── hello_pb_service.js

微妙にファイルの命名が違っていますが、hello_pb.js の型付けのために hello_pb.d.ts が生成されているのは同じですね。 ただこちらの方が Message の型付け度合いは以下のように充実しています。

export class HelloResponse extends jspb.Message {
  getMessage(): string;
  setMessage(value: string): void;

  hasProfile(): boolean;
  clearProfile(): void;
  getProfile(): Profile | undefined;
  setProfile(value?: Profile): void;

  serializeBinary(): Uint8Array;
  toObject(includeInstance?: boolean): HelloResponse.AsObject;
  static toObject(includeInstance: boolean, msg: HelloResponse): HelloResponse.AsObject;
  static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
  static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
  static serializeBinaryToWriter(message: HelloResponse, writer: jspb.BinaryWriter): void;
  static deserializeBinary(bytes: Uint8Array): HelloResponse;
  static deserializeBinaryFromReader(message: HelloResponse, reader: jspb.BinaryReader): HelloResponse;
}

toObject にも型が設定されているのが嬉しいですね。現状で選ぶならこちら一択だと思います。

protoc のオプション js_out の提供元

余談的なものになりますが、ts-protoc-gen や protoc-gen-grpc-web を比較しているときに両者が共通して利用している --js_out が気になりました。 protoc のデフォルトのオプションに含まれているので、元から含まれているものでしょうか。 となると JavaScript のコードジェネレータの実装も protoc に含まれているということなのでしょうか。

https://github.com/protocolbuffers/protobuf/tree/master/js がそれっぽいですね。

ただ gRPC のコードは含まれておらず単純にバイナリと JavaScript のデータの変換処理のみが行われているようです。 具体的には Protobuf の Message ごとに以下のメソッドを生やしているようです。

  • toObject
  • deserializeBinary
  • deserializeBinaryFromReader
  • serializeBinary
  • serializeBinaryToWriter

これ以外の処理は生成されないようなので完全にデータ変換のみの実装ですね。 なので protoc に含まれている JavaScript のコードジェネレータで生成されない gRPC 関係の service/server/client などの実装は ts-protoc-gen や protoc-gen-grpc-web などが独自に生成している、という理解をしました。