Skip to content

Latest commit

 

History

History
448 lines (317 loc) · 12.6 KB

generated_code.md

File metadata and controls

448 lines (317 loc) · 12.6 KB

Protobuf-ES: Generated Code

The following document describe how to generate, and what code precisely is generated for any given protobuf definition.

How to generate

We recommend buf as a protocol buffer compiler, but protoc works as well.

If you have the compiler set up, you can install the code generator plugin, as well as the accompanying runtime package @bufbuild/protobuf with:

npm install @bufbuild/protoc-gen-es @bufbuild/protobuf

This will install the code generator plugin in node_modules/.bin/protoc-gen-es. It is actually just a simple node script that selects the correct precompiled binary for your platform.

Generate with buf

To compile with buf, add a file buf.gen.yaml with the following content:

# Learn more: https://docs.buf.build/configuration/v1/buf-gen-yaml
version: v1
plugins:
  - plugin: es
    path: ./node_modules/.bin/protoc-gen-es
    opt: target=ts
    out: src/gen

Now buf generate will compile your .proto files to idiomatic TypeScript classes.

Generate with protoc

To compile with protoc:

protoc -I . --plugin ./node_modules/.bin/protoc-gen-es --es_out src/gen --es_opt target=ts example.proto

To learn about other ways to install the plugin, and about the available plugin options, see @bufbuild/protoc-gen-es.

Files

For every protobuf source file, we generate a corresponding .js, .ts, or .d.ts file, but add a _pb suffix to the name. For example, for the protobuf file foo/bar.proto, we generate foo/bar_pb.js.

By default, we generate JavaScript and TypeScript declaration files, so the generated code can be used in JavaScript or TypeScript projects without transpilation. If you prefer to generate TypeScript, use the plugin option [target=ts](https://github.com/bufbuild/protobuf-es/tree/main/packages/protoc-gen-es#target).

By default, we generate ECMAScript modules, which means we use import and export statements. If you need CommonJS, set the plugin option js_import_style=legacy_commonjs.

All import paths include a .js extension by default. You can remove or change the extension via the import_extension plugin option.

Messages

For the following message declaration:

message Example {}

we generate a class called Example, which extends the base class Message provided by @bufbuild/protobuf. See the runtime API documentation for details.

Note that some names cannot be used as class names and will be escaped by adding the suffix $. For example, a protobuf message break will become a class break$.

Field names

For each field declared in a message, we generate a property on the class. Note that property names are always lowerCamelCase, even if the corresponding protobuf field uses snake_case. While there is no official style for ECMAScript, most style guides (AirBnB, MDN, Google) as well as Node.js APIs and browser APIs use lowerCamelCase, and so do we.

Note that some names cannot be used as class properties and will be escaped by adding the suffix $. For example, a protobuf field constructor will become a class property constructor$.

Scalar fields

For these field definitions:

string foo = 1;
optional string bar = 2;

we will generate the following properties:

foo = "";
bar?: string;

Note that all scalar fields have an intrinsic default value in proto3 syntax, unless they are marked as optional. Protobuf types map to ECMAScript types as follows:

protobuf type ECMAScript type default value
double number 0
float number 0
int64 bigint 0n
uint64 bigint 0n
int32 number 0
fixed64 bigint 0n
fixed32 number 0
bool boolean false
string string ""
bytes Uint8Array new Uint8Array(0)
uint32 number 0
sfixed32 number 0
sfixed64 bigint 0n
sint32 number 0
sint64 bigint 0n

64-bit integral types

We use the BigInt primitive to represent 64-bit integral types. BigInt has been available in all major runtimes since 2020.

If you prefer to avoid BigInt in generated code, you can set the field option jstype = JS_STRING to generate String instead:

int64 my_field = 1 [jstype = JS_STRING]; // will generate `myField: string`

If BigInt is unavailable in your environment, Protobuf-ES falls back to the string representation. This means all values typed as bigint will be a string at runtime. For detailed information on how to handle both variants, see the conversion utility protoInt64 provided by @bufbuild/protobuf.

Message fields

For the following message field declaration:

message Example {
  Example field = 1;
}

we generate the following property:

field?: Example;

Note that we special case the well-known wrapper types: If a message uses google.protobuf.BoolValue for example, we automatically "unbox" the field to an optional primitive:

/**
 * @generated from field: google.protobuf.BoolValue bool_value_field = 1;
 */
boolValueField?: boolean;

Repeated fields

All repeated fields are represented with an ECMAScript Array. For example, the following field declaration:

repeated string field = 1;

is generated as:

field: string[] = [];

Note that all repeated fields will have an empty array as a default value.

Map fields

For the following map field declaration:

map<string, int32> field = 1;

we generate the property:

field: { [key: string]: number } = {};

Note that all map fields will have an empty object as a default value.

While it is not a perfectly clear-cut case, we chose to represent map fields as plain objects instead of ECMAScript map objects. While Map has better behavior around keys, they do not have a literal representation, do not support the spread operator and type narrowing in TypeScript.

Oneof groups

For the following oneof declaration:

message Example {
  oneof result {
    int32 number = 1;
    string error = 2;
  }
}

we generate the following property:

result:
  | { case: "number";  value: number }
  | { case: "error";   value: string }
  | { case: undefined; value?: undefined } = { case: undefined };

So the entire oneof group is turned into an object result with two properties:

  • case - the name of the selected field
  • value - the value of the selected field

Refer to the runtime API documentation for details on how to use this object.

Note: This feature requires the TypeScript compiler option strictNullChecks to be true. See the documentation for details.

Enumerations

For the following enum declaration:

enum Foo {
  DEFAULT_BAR = 0;
  BAR_BELLS = 1;
  BAR_B_CUE = 2;
}

we generate the following TypeScript enum:

enum Foo {
  DEFAULT_BAR = 0,
  BAR_BELLS = 1,
  BAR_B_CUE = 2
}

Note that some names cannot be used as enum names and will be escaped by adding the suffix $. For example, a protobuf enum catch will become a TypeScript enum catch$.

If all enum values share a prefix that corresponds with the enum's name, the prefix is dropped from all enum value names. For example, for the following enum declaration:

enum Foo {
  FOO_BAR = 0;
  FOO_BAZ = 1;
}

we generate the following TypeScript enum:

enum Foo {
   BAR = 0,
   BAZ = 1
}

Extensions

An extension is a field defined outside of its container message. For example:

extend Example {
  optional string extra_string = 1001;
}

For this extension, we generate:

const extra_string: Extension<Example, string>;

See the runtime API documentation for details on how to access extension values.

Services

protoc-gen-es does not generate any code for service declarations.

Groups

Groups are a deprecated feature in proto2 that allows to declare a field and a message at the same time:

message MessageWithGroup {
  optional group MyGroup = 1 {
    optional int32 int32_field = 1;
  }
}

For this group field, we generate the following property:

mygroup?: MessageWithGroup_MyGroup;

We also generate the message class MessageWithGroup_MyGroup.

Nested types

A message, enum, or extension can be declared within a message. For example:

message Example {
  message Message {}
  enum Enum {ENUM_UNSPECIFIED = 0;}
  extend SomeMessage { optional bool enabled = 1; }
}

Since ECMAScript doesn't have a concept of inner classes like Java or C#, the nested types will be prefixed with the parent message name: We generate an empty message class Example, a nested message class Example_Message, a nested enum Example_Enum, and a nested extension Example_enabled.

Comments

We think that your comments in proto sources files are important, and take great care to carry them over to the generated code as JSDocs comments. That includes license headers in your file, as well as comments down to individual enum values, for example.

Preamble

Each generated file contains a preamble with information about the source file, and how it was generated:

// @generated by protoc-gen-es v1.0.0
// @generated from file comments.proto (package spec, syntax proto3)
/* eslint-disable */
/* @ts-nocheck */

To improve forwards and backwards compatibility, we add the annotations to disable eslint and type checking through the TypeScript compiler.

Element comments

We generate similar information for every single protobuf element, so you always have the best possible transparency:

@generated from field: map<string, bytes> str_bytes_field = 5;

Deprecation

We support the deprecated option for all elements. For example, for the following field declaration:

// This field is deprecated
string deprecated_field = 1 [deprecated = true];

we generate:

/**
 * This field is deprecated
 *
 * @generated from field: string deprecated_field = 1 [deprecated = true];
 * @deprecated
 */
deprecatedField = "";

If you mark a file as deprecated, we generate @deprecated JSDoc tags for all symbols in this file.