The following document describe how to generate, and what code precisely is generated for any given protobuf definition.
- How to generate
- Files
- Messages
- Field names
- Scalar fields
- 64-bit integral types
- Message fields
- Repeated fields
- Map fields
- Oneof groups
- Enumerations
- Extensions
- Nested types
- Services
- Comments
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.
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.
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.
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
.
Note that we generate ECMAScript modules, which means we use import
and export
statements.
All import paths include a .js
extension, so you can use the generated code in Node.js
with "type": "module"
in your project's package.json
without transpilation.
If you do require support for the legacy CommonJS format, you can generate TypeScript and
transpile it, for example with the extremely fast esbuild
bundler.
It is also possible to modify the extension used in the import paths via the
import_extension
plugin option.
This option allows you to choose which extension will used in the imports,
providing flexibility for different environments.
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$
.
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$
.
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 |
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.
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;
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.
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.
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 fieldvalue
- 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.
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
}
We do not support extensions (a proto2 feature) at this time.
A message or enum can be declared within a message. For example:
message Example {
message Message {}
enum Enum {ENUM_UNSPECIFIED = 0;}
}
Since ECMAScript doesn't have a concept of inner classes like Java or C#, we generate the
two classes Example
and Example_Message
, as well as the enum Example_Enum
.
protoc-gen-es
does not generate any code for service declarations.
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.
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.
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;
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.