Free & open source — no account required

v1.2.5-PRICING-19
Web & Frontend • Engineering Documentation

Protobuf to TypeScript Converter

This technical guide provides an in-depth analysis of the protobuf to typescript engine, best practices for implementation, and data security standards.

Protobuf to TypeScript: Buf CLI, Connect-ES, and Generated gRPC Clients

Protocol Buffers are the IDL (Interface Description Language) for gRPC — but generating TypeScript from .proto files is where most teams get stuck. There are two generations of tooling: the legacy protoc compiler with various plugins (protoc-gen-ts, ts-protoc-gen), and the modern Buf ecosystem (buf generate with @bufbuild/protoc-gen-es). The Buf approach is significantly simpler to configure, generates idiomatic TypeScript (ES2015+ classes instead of CommonJS), and integrates with Connect-ES for browser-compatible Protobuf over HTTP/1.1. This guide covers the Buf approach, the generated type structure, and how to use generated types to call gRPC services in both Node.js and the browser.

Live Example: .proto Definition and Generated TypeScript

// user.proto (Protobuf v3)
syntax = "proto3";
package users.v1;

import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";

message User {
  string  id         = 1;
  string  username   = 2;
  string  email      = 3;
  Role    role       = 4;
  bool    is_active  = 5;
  repeated string tags = 6;
  google.protobuf.Timestamp created_at = 7;
  // optional: present = value is set, absent = not provided
  optional string bio = 8;
}

enum Role {
  ROLE_UNSPECIFIED = 0;   // proto3 enum must have a 0 value
  ROLE_VIEWER      = 1;
  ROLE_EDITOR      = 2;
  ROLE_ADMIN       = 3;
}

service UserService {
  rpc GetUser    (GetUserRequest)    returns (User);
  rpc ListUsers  (ListUsersRequest)  returns (ListUsersResponse);
  rpc CreateUser (CreateUserRequest) returns (User);
  rpc WatchUsers (WatchUsersRequest) returns (stream User);  // server streaming
}

message GetUserRequest    { string id = 1; }
message ListUsersRequest  { int32 page = 1; int32 limit = 2; Role role_filter = 3; }
message ListUsersResponse { repeated User users = 1; int32 total = 2; }
message CreateUserRequest { string username = 1; string email = 2; Role role = 3; }
message WatchUsersRequest { bool active_only = 1; }

// Generated TypeScript (by @bufbuild/protoc-gen-es)
// user_pb.ts — generated, do not edit
import { Message, proto3 } from "@bufbuild/protobuf";
import { Timestamp } from "@bufbuild/protobuf/dist/cjs/google/protobuf/timestamp_pb";

export enum Role {
  UNSPECIFIED = 0,
  VIEWER      = 1,
  EDITOR      = 2,
  ADMIN       = 3,
}

export class User extends Message<User> {
  id:        string   = "";
  username:  string   = "";
  email:     string   = "";
  role:      Role     = Role.UNSPECIFIED;
  isActive:  boolean  = false;  // snake_case → camelCase
  tags:      string[] = [];
  createdAt?: Timestamp;        // google.protobuf.Timestamp
  bio?:       string;           // optional field — may be undefined
}

Setting Up Buf CLI

# Install Buf
npm install -D @bufbuild/buf @bufbuild/protoc-gen-es @bufbuild/protobuf

# buf.gen.yaml — code generation config
version: v1
plugins:
  - plugin: es                   # generates message types (TypeScript)
    out: src/gen
    opt: target=ts               # TypeScript output
  - plugin: connect-es           # generates service clients
    out: src/gen
    opt: target=ts

# buf.yaml — lint and breaking change detection config
version: v1
lint:
  use:
    - DEFAULT
breaking:
  use:
    - FILE   # check against latest on main branch

# package.json scripts
{
  "scripts": {
    "proto:generate": "buf generate",
    "proto:lint":     "buf lint",
    "proto:breaking": "buf breaking --against '.git#branch=main'"
  }
}

# Generate TypeScript from .proto files
npm run proto:generate
# Outputs:
# src/gen/users/v1/user_pb.ts          (message types)
# src/gen/users/v1/user_connect.ts     (service client)

Using Generated Clients: Connect-ES (Browser + Node.js)

// Connect-ES works in both the browser (via fetch) and Node.js (via Node fetch)
import { createPromiseClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { UserService } from "./gen/users/v1/user_connect";
import { Role } from "./gen/users/v1/user_pb";

// Create transport — uses standard fetch, works in browsers
const transport = createConnectTransport({
  baseUrl: "https://api.example.com",
});

const client = createPromiseClient(UserService, transport);

// Get a user — request and response are fully typed
async function getUser(id: string) {
  const user = await client.getUser({ id });
  // user.id:        string
  // user.username:  string
  // user.role:      Role (enum value)
  // user.isActive:  boolean
  // user.tags:      string[]
  // user.createdAt: Timestamp | undefined
  // user.bio:       string | undefined  (optional field)
  return user;
}

// List users with filter
async function listAdmins() {
  const response = await client.listUsers({
    page:       1,
    limit:      20,
    roleFilter: Role.ADMIN,
  });
  response.users.forEach(u => console.log(u.username));
  return response;
}

// Server-side streaming (returns async iterable)
async function watchUsers() {
  const stream = client.watchUsers({ activeOnly: true });
  for await (const user of stream) {
    console.log(`User ${user.username} changed`);
  }
}

Node.js gRPC with @grpc/grpc-js (Alternative)

// For pure gRPC (non-Connect) in Node.js
// buf.gen.yaml — add grpc-node plugin
plugins:
  - plugin: es
    out: src/gen
    opt: target=ts
  - plugin: grpc/node
    out: src/gen

// Generated client usage
import * as grpc from "@grpc/grpc-js";
import { UserServiceClient } from "./gen/users/v1/user_grpc_pb";
import { GetUserRequest } from "./gen/users/v1/user_pb";

const client = new UserServiceClient(
  "localhost:50051",
  grpc.credentials.createInsecure()
);

async function getUser(id: string): Promise<User> {
  return new Promise((resolve, reject) => {
    const req = new GetUserRequest();
    req.setId(id);

    client.getUser(req, (err, response) => {
      if (err) reject(err);
      else resolve(response);
    });
  });
}

Handling Well-Known Types

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

// google.protobuf.Timestamp ↔ JavaScript Date
const tsProto = Timestamp.fromDate(new Date('2024-01-15T08:30:00Z'));
const jsDate  = tsProto.toDate();

// google.protobuf.Value — dynamic JSON value in Protobuf
// Useful for metadata fields with unknown structure
import { Value } from "@bufbuild/protobuf";
const value = Value.fromJson({ name: "Alice", active: true });

// google.protobuf.Struct — JSON object
import { Struct } from "@bufbuild/protobuf";
const struct = Struct.fromJson({ key: "value" });
const json   = struct.toJson();

// In .proto:
// message Event {
//   google.protobuf.Struct  metadata = 5;   // arbitrary JSON object
//   google.protobuf.Value   payload  = 6;   // any JSON value
// }

Breaking Change Detection with Buf

# buf breaking checks if your .proto changes break existing clients
# Field number changes, field type changes, and service method removals are breaking

# Check against the main branch
buf breaking --against '.git#branch=main'

# Breaking changes that Buf catches:
# - Removing or renaming a field
# - Changing a field number
# - Changing field type (string → int)
# - Removing an enum value used by existing clients
# - Changing a service method signature

# NOT breaking (backwards compatible):
# - Adding a new field (existing clients ignore unknown fields)
# - Adding a new enum value
# - Adding a new RPC method
# - Adding a new message type

# CI/CD: fail the build if breaking changes are detected
buf breaking --against "https://github.com/myorg/myproto.git#branch=main"

Best Practices for Production

  • Use the Buf CLI instead of raw protoc: Buf handles module dependencies, lint rules, and breaking change detection that would require separate tools with protoc. It also has a managed dependency registry (Buf Schema Registry) for sharing .proto files across teams.
  • Never reuse or change field numbers: Field numbers are the wire format identifier. If you remove a field, mark it as reserved 5; in the message to prevent the number from being reused in the future — this prevents silent data corruption when old clients send data with that field number.
  • Prefer Connect-ES over gRPC-web for browser clients: Connect-ES uses standard HTTP/1.1 or HTTP/2 with JSON or Protobuf — no Envoy proxy required. It supports the same streaming semantics as gRPC but works directly with any HTTP server. gRPC-web requires a proxy layer.
  • Use optional for fields that need presence detection: In proto3, regular fields are always present with default values — you can't distinguish "0" from "not set". Use optional string name = 1; when absence is semantically different from an empty string.

FAQ

Q: What's the difference between protoc-gen-ts and @bufbuild/protoc-gen-es?
A: @bufbuild/protoc-gen-es is the official Buf generator — it produces modern ES module TypeScript with proper class inheritance from Message<T>, tree-shaking support, and browser compatibility. protoc-gen-ts is a community plugin with more varied TypeScript output. For new projects, use the Buf ecosystem.

Q: Can I use Protobuf without gRPC?
A: Yes — Protobuf is just a serialization format. You can use it over REST HTTP/JSON-to-Protobuf gateways, WebSockets, or message queues (Kafka, Pub/Sub). The generated TypeScript classes have toBinary() / fromBinary() and toJson() / fromJson() methods for explicit encoding/decoding.

Q: How do I handle enum default values in proto3?
A: Proto3 requires enums to have a zero value (e.g., ROLE_UNSPECIFIED = 0). When a field is not set, it defaults to 0. Design your enums with a meaningful UNSPECIFIED or UNKNOWN value at 0 so your application code can explicitly handle the "not set" case.

Developer FAQ

Is the processing local-only?

Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.

Can I use this for enterprise projects?

Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.