Repository: nathanhleung/protobuf-ts-types Branch: main Commit: 46e83d2b9afd Files: 13 Total size: 18.2 KB Directory structure: gitextract_yi7hy7a5/ ├── .gitignore ├── README.md ├── examples/ │ ├── README.md │ └── basic/ │ ├── .gitignore │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── package.json ├── src/ │ ├── array.ts │ ├── index.ts │ ├── proto.ts │ └── string.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # vitepress build output **/.vitepress/dist # vitepress cache directory **/.vitepress/cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* ================================================ FILE: README.md ================================================ # protobuf-ts-types > Zero-codegen, no-compile TypeScript `type` inference from protobuf `message`s. `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](https://github.dev/nathanhleung/protobuf-ts-types/blob/main/examples/basic/index.ts) | [View on CodeSandbox](https://codesandbox.io/p/github/nathanhleung/protobuf-ts-types/main?import=true&embed=1&file=%2Fexamples%2Fbasic%2Findex.ts) | [Discuss on Hacker News](https://news.ycombinator.com/item?id=43682547) > [!WARNING] > Proof of concept, not production ready. See [Limitations](#limitations) below for more details. Screenshot ## How it Works In short, aggressive use of TypeScript's [template literal types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html). Annotated example from the source: ```ts // Pass the proto string you want to infer `message` names from as a generic parameter type MessageNames = // Infer `message` parts using template literal type WrapWithNewlines extends `${string}${Whitespace}message${Whitespace}${infer MessageName}${OptionalWhitespace}{${string}}${infer Rest}` ? // Recursively infer remaining message names [MessageName, ...MessageNames] : []; ``` See more in [`src/proto.ts`](./src/proto.ts). ## Usage First, install the package. ``` npm install https://github.com/nathanhleung/protobuf-ts-types ``` Then, use it in TypeScript. ```ts 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; type Person = Proto["Person"]; type Person2 = pbt.infer; // `Person` and `Person2` are the same type: // ``` // { // name: string; // id: number; // is_ceo: boolean; // description?: string; // } // ``` type Group = pbt.infer; 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) // ``` ``` ## Limitations * If not using inline (i.e., literals in TypeScript) proto `string`s `as const`, probably requires a [`ts-patch`](https://github.com/nonara/ts-patch) compiler patch to import `.proto` files until https://github.com/microsoft/TypeScript/issues/42219 is resolved * `service`s and `rpc`s are not supported (only `message`s) * `oneof` and `map` fields are not supported * `import`s are not supported (for now, concatenate) ## API ### `pbt` Top-level exported namespace. ``` import { pbt } from "protobuf-ts-types"; ``` ### `pbt.infer` Given a proto source string, infers the types of the `message`s in the source. #### Returns * 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`. ================================================ FILE: examples/README.md ================================================ # protobuf-ts-types Examples ## Usage Open the directory of the example you want to run in VSCode, then run ``` npm install npm start ``` ================================================ FILE: examples/basic/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # vitepress build output **/.vitepress/dist # vitepress cache directory **/.vitepress/cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* ================================================ FILE: examples/basic/index.ts ================================================ import { pbt } from "../../src/index"; 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; } `; type Proto = pbt.infer; type Person = Proto["Person"]; type Group = 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(); } } 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, }, ], }); ================================================ FILE: examples/basic/package.json ================================================ { "name": "@protobuf-ts-types/example-basic", "version": "1.0.0", "main": "index.ts", "private": true, "scripts": { "start": "tsx index.ts" }, "author": "nathanhleung", "license": "Apache-2.0", "description": "", "devDependencies": { "tsx": "^4.19.3", "typescript": "^5.8.3" } } ================================================ FILE: examples/basic/tsconfig.json ================================================ { "extends": "../../tsconfig.json", } ================================================ FILE: package.json ================================================ { "name": "protobuf-ts-types", "author": "nathanhleung", "version": "0.0.1", "description": "Zero-codegen TypeScript `type` inference from protobuf `message`s", "keywords": [ "protobuf", "protobufjs", "protobuf-es", "proto", "buf", "typescript", "type", "inference", "codegen", "grpc", "grpc-web", "connect" ], "main": "src/index.ts", "homepage": "https://github.com/nathanhleung/protobuf-ts-types#readme", "repository": { "type": "git", "url": "git+https://github.com/nathanhleung/protobuf-ts-types.git" }, "license": "Apache-2.0", "devDependencies": { "@types/node": "^22.14.1", "typescript": "^5.8.3" }, "dependencies": { "type-fest": "^4.39.1" } } ================================================ FILE: src/array.ts ================================================ import type { Trim } from "type-fest"; /** * Trims leading and trailing whitespace from each element of the given array * of strings. */ export type MapTrim = Strings extends [ infer Head, ...infer Tail ] ? Head extends string ? Tail extends string[] ? [Trim, ...MapTrim] : [] : [] : []; /** * Removes empty strings from the given array of strings. */ export type FilterEmpty = Strings extends [ infer Head, ...infer Tail ] ? Head extends "" ? FilterEmpty : [Head, ...FilterEmpty] : []; /** * Given an array of objects, creates an object type where the keys are the * values of the given key for each object. Think of it as a combination of * lodash's [`keyBy`](https://lodash.com/docs/4.17.15#keyBy) and * [`pick`](https://lodash.com/docs/4.17.15#pick). */ export type KeyBy< Objects extends { [key in Key]: string | number | symbol }[], Key extends string > = { [Object in Objects[number] as Object[Key]]: Object; }; ================================================ FILE: src/index.ts ================================================ import type { infer as Infer } from "./proto"; export namespace pbt { export type infer< Proto extends string, MessageName extends string = "" > = Infer; } ================================================ FILE: src/proto.ts ================================================ import type { Split, Trim } from "type-fest"; import type { FilterEmpty, KeyBy, MapTrim } from "./array"; import type { OptionalWhitespace, StringToNumber, Whitespace, WrapWithNewlines, } from "./string"; /** * Extracts message names from the given proto string. */ type MessageNames = WrapWithNewlines extends `${string}${Whitespace}message${Whitespace}${infer MessageName}${OptionalWhitespace}{${string}}${infer Rest}` ? [MessageName, ...MessageNames] : []; /** * Extracts field definitions of the given message name. */ type RawFieldDefinitions< Proto extends string, MessageName extends MessageNames[number] > = WrapWithNewlines extends `${string}${Whitespace}message${Whitespace}${MessageName}${OptionalWhitespace}{${infer FieldDefinitions}}${string}` ? FilterEmpty>> : []; /** * Given a raw field definition extracted by `RawFieldDefinitions`, parses it * into an object type. */ type ParseRawFieldDefinition< Proto extends string, RawFieldDefinition extends string > = RawFieldDefinition extends `${infer FieldCardinality}${Whitespace}${infer FieldType}${Whitespace}${infer FieldName}${OptionalWhitespace}=${OptionalWhitespace}${infer FieldNumber}` ? FieldName extends "" ? RawFieldDefinition extends `${infer FieldType}${Whitespace}${infer FieldName}${OptionalWhitespace}=${OptionalWhitespace}${infer FieldNumber}` ? { type: ParseFieldType>; name: Trim; number: StringToNumber>; } : never : { type: ParseFieldType, Trim>; name: Trim; number: StringToNumber>; } : never; /** * Given a raw field type, parses it into a TypeScript type, handling * cardinality appropriately. */ type ParseFieldType< Proto extends string, RawFieldType extends string, Cardinality extends string = "" > = Cardinality extends "optional" ? ParseRawFieldType | undefined : Cardinality extends "repeated" ? ParseRawFieldType[] : ParseRawFieldType; /** * Given a raw field type, parses it into a TypeScript type. */ type ParseRawFieldType< Proto extends string, RawType extends string > = RawType extends "string" ? string : RawType extends "bool" ? boolean : RawType extends "bytes" ? Uint8Array : RawType extends "float" ? number : RawType extends "double" ? number : RawType extends "int32" ? number : RawType extends "int64" ? number : RawType extends "uint32" ? number : RawType extends "uint64" ? number : MessagesByMessageName[RawType]; /** * Given raw field definitions extracted by `RawFieldDefinitions`, parses them * into object types using `ParseRawFieldDefinition`. */ type ParseRawFieldDefinitions< Proto extends string, RawDefinitions extends string[] > = RawDefinitions extends [infer Head, ...infer Tail] ? Head extends string ? Tail extends string[] ? [ ParseRawFieldDefinition, ...ParseRawFieldDefinitions ] : [] : [] : []; /** * Extracts field definitions for a given message name. */ type FieldDefinitions< Proto extends string, MessageName extends MessageNames[number] > = ParseRawFieldDefinitions>; /** * Extracts a mapping of field names to field definitions for a given message * name. */ type FieldDefinitionsByFieldName< Proto extends string, MessageName extends MessageNames[number] > = KeyBy, "name">; /** * Extracts the field names for a given message name. */ type FieldNames< Proto extends string, MessageName extends MessageNames[number] > = keyof FieldDefinitionsByFieldName; /** * Infers the type of the named message in the given proto string. */ type MessageType< Proto extends string, MessageName extends MessageNames[number] > = { [k in FieldNames< Proto, MessageName > as undefined extends FieldDefinitionsByFieldName< Proto, MessageName >[k]["type"] ? never : k]: FieldDefinitionsByFieldName[k]["type"]; } & { [k in FieldNames< Proto, MessageName > as undefined extends FieldDefinitionsByFieldName< Proto, MessageName >[k]["type"] ? k : never]?: FieldDefinitionsByFieldName[k]["type"]; }; /** * Creates a mapping of message names to their respective inferred types for * the given proto string. */ type MessagesByMessageName = { [k in MessageNames[number]]: MessageType; }; export type infer< Proto extends string, MessageName extends string = "" > = MessageName extends "" ? MessagesByMessageName : MessageName extends MessageNames[number] ? MessagesByMessageName[MessageName] : never; ================================================ FILE: src/string.ts ================================================ /** Wraps the given string type with newlines. */ export type WrapWithNewlines = `\n${T}\n`; /** A space, tab, newline, or carriage return. */ export type Whitespace = " " | "\t" | "\n" | "\r"; export type OptionalWhitespace = "" | Whitespace; export type StringToNumber = T extends `${infer N extends number}` ? N : never; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "CommonJS", "lib": ["ES2024"], "types": ["node"], "target": "ES2024", "allowJs": false, "noEmit": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true } }