Closed11

新しいprotobuf for Typescriptのprotobuf-esについて調べてみる

かいものかいもの

そもそものProtobufを取り巻く世界

protoファイルからTypescriptのエンティティを生成するためには
protobuf-tsprotoc-ts-genを利用して生成していました。
https://github.com/timostamm/protobuf-ts
https://github.com/improbable-eng/ts-protoc-gen

しかし、protobuf周りのエコシステムがイケてない点がありました。

  • googleapisなどのよく使う定義(Well-known types)や別リポジトリのprotoなどの依存解決などは自前でなんとかする必要があった。(npm的なやつがなかった)
    https://github.com/googleapis/googleapis

  • ビルド時のコマンドを管理することが難しかった

  • 何をするにもそれぞれのコマンドをインストールする必要があった

👆みたいな感じで、結構頑張らないと環境を構成するのが面倒でした。

かいものかいもの

Bufの出現

そこで、Bufという新しいProtocol Buffersのエコシステムが台頭してきました。

BufでProtobufをTypescriptに変換するには、古いprotoc-gen-tsは非推奨で、protoc-gen-esへの移行を促されました。

かいものかいもの

-ts から -es へ

protoc-gen-esもBufがメンテしているっぽい。
最近GoogleのProtobufがテキトーすぎて、公式ページ移行しましたね

https://github.com/bufbuild/protobuf-es

protobuf-tsprotobuf-esで生成されるコードについて言及し、
思想を紐解いていきます

かいものかいもの

①エンティティはInterfaceではなく、Classで出力されます

Protobufのエンコード仕様 === Interfaceの定義、とすることが難しいため
Interfaceでは、Protobufへのシリアライズなどの追加機能(主にProotbufのエンコード仕様を吸収するための)を、標準として提供することがむずかしいため。

const request1 = Hello.fromJsonString('{"msg": "hello"}')
const request2 = new Hello({"msg": "hello"})
request.toBinary()

👆こんな感じでリッチな機能が追加で提供される

かいものかいもの

②Interfaceとしてほしい場合はPartialMessage<T>もしくはPlainMessage<T>

使い分けPartialMessage<T>

ネストしているフィールドに対して再帰的にTypescriptのUtilityTypesのPartialと同じ動きをする。

message User {
  string firstName = 1;
  string lastName = 2;
}

の定義に対し、

const u = new User();
u.firstName = "Homer";

が許される
この時、lastName=""となる

かいものかいもの

②oneof
Protobufのフィールドにoneofをつけてコンパイルすると、

message User {
  ...
  oenof result {
    int32 number = 1;
    string error =2;
  }
}

caseフィールドが追加されて、その値を基に判断する。
io-ts感のある仕様。

user.result = {case: "number", value: 123};
user.result = { case: "error";   value: "invalid" };
user.result = {case: undefined};
かいものかいもの

③比較演算
生成したクラス自身に、比較用メソッドが実装されていて、同じインスタンス同士の中身比較を行える

user.equals(user); // true
user.equals(null); // false
User.equals(user, user); // true 
User.equals(user, null); // false 
かいものかいもの

④シリアライザ
いろんなフォーマットへの変換をサポートしている
E2Eテスト書くときとか便利そう

// to Binary
const bytes: Uint8Array = user.toBinary();
User.fromBinary(bytes);
// to Base64
import { protoBase64 } from '@bufbuild/protobuf';
const bytes: Uint8Array = user.toBinary();
const base64: string = protoBase64.enc(bytes)
User.fromBinary(protoBase64.dec(base64));
// to JSON
const json = user.toJson();
User.fromJson(json);
// stringifyと同じ機能
const json = user.toJsonString();
User.fromJsonString(json);
かいものかいもの

⑤Well known types
これは便利そう。直感的に使えてかなりいい感じ

import { Timestamp } from "@bufbuild/protobuf";

// Create an instance from a built-in Date object
let ts = Timestamp.fromDate(new Date(1938, 0, 10));

// Create an instance with the current time
ts = Timestamp.now()

// Convert to a built-in Date object
ts.toDate();
かいものかいもの

⑥Message<T>の全貌
runtime:proto2/proto3を判定できる。(使い道はわからない…)
typeNamepackage Name+"."+MessageName
fields:残りのフィールドがフィールド番号と型と一緒に含められてる

class User extends Message<User> {
  //...
  static readonly runtime = proto3;
  static readonly typeName = "docs.User";
  static readonly fields: FieldList = proto3.util.newFieldList(() => [
    { no: 1, name: "first_name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
    { no: 2, name: "last_name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
    { no: 3, name: "active", kind: "scalar", T: 8 /* ScalarType.BOOL */ },
    { no: 4, name: "manager", kind: "message", T: User },
    { no: 5, name: "locations", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true },
    { no: 6, name: "projects", kind: "map", K: 9 /* ScalarType.STRING */, V: {kind: "scalar", T: 9 /* ScalarType.STRING */} },
  ]);
このスクラップは2023/01/31にクローズされました