Show HN: Zero-codegen, no-compile TypeScript type inference from Protobufs

2 months ago 2

Zero-codegen, no-compile TypeScript type inference from protobuf messages.

protobuf-ts-types lets you define language-agnostic message types in proto format, then infers TypeScript types from them with no additional codegen.

Try on github.dev | View on CodeSandbox

Warning

Proof of concept, not production ready. See Limitations below for more details.

Screenshot

In short, aggressive use of TypeScript's template literal types. Annotated example from the source:

// Pass the proto string you want to infer `message` names from as a generic parameter type MessageNames<Proto extends string> = // Infer `message` parts using template literal type WrapWithNewlines<Proto> extends `${string}${Whitespace}message${Whitespace}${infer MessageName}${OptionalWhitespace}{${string}}${infer Rest}` ? // Recursively infer remaining message names [MessageName, ...MessageNames<Rest>] : [];

See more in src/proto.ts.

First, install the package.

npm install https://github.com/nathanhleung/protobuf-ts-types

Then, use it in TypeScript.

import { pbt } from "protobuf-ts-types"; const proto = ` syntax = "proto3"; message Person { string name = 1; int32 id = 2; bool is_ceo = 3; optional string description = 4; } message Group { string name = 1; repeated Person people = 2; } `; // `Proto` is a mapping of message names to message types, inferred from the // `proto` source string above. type Proto = pbt.infer<typeof proto>; type Person = Proto["Person"]; type Person2 = pbt.infer<typeof proto, "Person">; // `Person` and `Person2` are the same type: // ``` // { // name: string; // id: number; // is_ceo: boolean; // description?: string; // } // ``` type Group = pbt.infer<typeof proto, "Group">; function greetPerson(person: Person) { console.log(`Hello, ${person.name}!`); if (person.description) { console.log(`${person.description}`); } else { console.log("(no description)"); } } function greetGroup(group: Group) { console.log(`=========${"=".repeat(group.name.length)}===`); console.log(`= Hello, ${group.name}! =`); console.log(`=========${"=".repeat(group.name.length)}===`); for (const person of group.people) { greetPerson(person); console.log(); } } // If the structure of the `Group` or any of the individual `Person`s does not // match the type, TypeScript will show an error. greetGroup({ name: "Hooli", people: [ { name: "Gavin Belson", id: 0, is_ceo: true, description: "CEO of Hooli", }, { name: "Richard Hendricks", id: 1, is_ceo: true, description: "CEO of Pied Piper", }, { name: "Dinesh Chugtai", id: 2, is_ceo: false, description: "Software Engineer", }, { name: "Jared Dunn", id: 3, is_ceo: false, }, ], }); // Output: // ``` // ================= // = Hello, Hooli! = // ================= // Hello, Gavin Belson! // CEO of Hooli // Hello, Richard Hendricks! // CEO of Pied Piper // Hello, Dinesh Chugtai! // Software Engineer // Hello, Jared Dunn! // (no description) // ```
  • If not using inline (i.e., literals in TypeScript) proto strings as const, probably requires a ts-patch compiler patch to import .proto files until microsoft/TypeScript#42219 is resolved
  • services and rpcs are not supported (only messages)
  • oneof and map fields are not supported
  • imports are not supported (for now, concatenate)

Top-level exported namespace.

import { pbt } from "protobuf-ts-types";

pbt.infer<Proto extends string, MessageName extends string = "">

Given a proto source string, infers the types of the messages in the source.

  • If MessageName is an empty string, the returned type is a mapping from message names to message types.
  • If MessageName is a known message, the returned type is the inferred type of the given MessageName.
  • If MessageName is not a known message, the returned type is never.
Read Entire Article