新しいprotobuf for Typescriptのprotobuf-esについて調べてみる
そもそものProtobufを取り巻く世界
protoファイルからTypescriptのエンティティを生成するためには
protobuf-ts
やprotoc-ts-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がテキトーすぎて、公式ページ移行しましたね
protobuf-ts
とprotobuf-es
で生成されるコードについて言及し、
思想を紐解いていきます
①エンティティはInterfaceではなく、Classで出力されます
Protobufのエンコード仕様 === Interfaceの定義、とすることが難しいため
Interfaceでは、Protobufへのシリアライズなどの追加機能(主にProotbufのエンコード仕様を吸収するための)を、標準として提供することがむずかしいため。
const request1 = Hello.fromJsonString('{"msg": "hello"}')
const request2 = new Hello({"msg": "hello"})
request.toBinary()
👆こんな感じでリッチな機能が追加で提供される
PartialMessage<T>
もしくはPlainMessage<T>
②Interfaceとしてほしい場合は
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を判定できる。(使い道はわからない…)
typeName
:package 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 */} },
]);
総括すると
protobuf-ts
から引っ越すガイドライン👇