[
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n.cache\n\n# vitepress build output\n**/.vitepress/dist\n\n# vitepress cache directory\n**/.vitepress/cache\n\n# Docusaurus cache and generated files\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n"
  },
  {
    "path": "README.md",
    "content": "# protobuf-ts-types\n\n> Zero-codegen, no-compile TypeScript `type` inference from protobuf `message`s.\n\n`protobuf-ts-types` lets you define language-agnostic `message` types in `proto` format, then infers TypeScript types from them with no additional codegen.\n\n[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)\n\n> [!WARNING]\n> Proof of concept, not production ready. See [Limitations](#limitations) below for more details.\n\n<img src=\"./screenshot.png\" width=\"400px\" alt=\"Screenshot\" style=\"border: 1px solid #eee;\">\n\n## How it Works\n\nIn 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:\n\n```ts\n// Pass the proto string you want to infer `message` names from as a generic parameter\ntype MessageNames<Proto extends string> =\n  // Infer `message` parts using template literal type\n  WrapWithNewlines<Proto> extends `${string}${Whitespace}message${Whitespace}${infer MessageName}${OptionalWhitespace}{${string}}${infer Rest}`\n    ? // Recursively infer remaining message names\n      [MessageName, ...MessageNames<Rest>]\n    : [];\n```\n\nSee more in [`src/proto.ts`](./src/proto.ts).\n\n## Usage\n\nFirst, install the package.\n\n```\nnpm install https://github.com/nathanhleung/protobuf-ts-types\n```\n\nThen, use it in TypeScript.\n\n```ts\nimport { pbt } from \"protobuf-ts-types\";\n\nconst proto = `\n    syntax = \"proto3\";\n\n    message Person {\n      string name = 1;\n      int32 id = 2;\n      bool is_ceo = 3;\n      optional string description = 4;\n    }\n\n    message Group {\n        string name = 1;\n        repeated Person people = 2;\n    }\n`;\n\n// `Proto` is a mapping of message names to message types, inferred from the\n// `proto` source string above.\ntype Proto = pbt.infer<typeof proto>;\n\ntype Person = Proto[\"Person\"];\ntype Person2 = pbt.infer<typeof proto, \"Person\">;\n\n// `Person` and `Person2` are the same type:\n// ```\n// {\n//     name: string;\n//     id: number;\n//     is_ceo: boolean;\n//     description?: string;\n// }\n// ```\n\ntype Group = pbt.infer<typeof proto, \"Group\">;\n\nfunction greetPerson(person: Person) {\n  console.log(`Hello, ${person.name}!`);\n\n  if (person.description) {\n    console.log(`${person.description}`);\n  } else {\n    console.log(\"(no description)\");\n  }\n}\n\nfunction greetGroup(group: Group) {\n  console.log(`=========${\"=\".repeat(group.name.length)}===`);\n  console.log(`= Hello, ${group.name}! =`);\n  console.log(`=========${\"=\".repeat(group.name.length)}===`);\n\n  for (const person of group.people) {\n    greetPerson(person);\n    console.log();\n  }\n}\n\n// If the structure of the `Group` or any of the individual `Person`s does not\n// match the type, TypeScript will show an error.\ngreetGroup({\n  name: \"Hooli\",\n  people: [\n    {\n      name: \"Gavin Belson\",\n      id: 0,\n      is_ceo: true,\n      description: \"CEO of Hooli\",\n    },\n    {\n      name: \"Richard Hendricks\",\n      id: 1,\n      is_ceo: true,\n      description: \"CEO of Pied Piper\",\n    },\n    {\n      name: \"Dinesh Chugtai\",\n      id: 2,\n      is_ceo: false,\n      description: \"Software Engineer\",\n    },\n    {\n      name: \"Jared Dunn\",\n      id: 3,\n      is_ceo: false,\n    },\n  ],\n});\n\n// Output:\n// ```\n// =================\n// = Hello, Hooli! =\n// =================\n// Hello, Gavin Belson!\n// CEO of Hooli\n\n// Hello, Richard Hendricks!\n// CEO of Pied Piper\n\n// Hello, Dinesh Chugtai!\n// Software Engineer\n\n// Hello, Jared Dunn!\n// (no description)\n// ```\n```\n\n## Limitations\n\n* 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\n* `service`s and `rpc`s are not supported (only `message`s)\n* `oneof` and `map` fields are not supported\n* `import`s are not supported (for now, concatenate)\n\n## API\n\n### `pbt`\n\nTop-level exported namespace.\n\n```\nimport { pbt } from \"protobuf-ts-types\";\n```\n\n### `pbt.infer<Proto extends string, MessageName extends string = \"\">`\n\nGiven a proto source string, infers the types of the `message`s in the source.\n\n#### Returns\n\n* If `MessageName` is an empty string, the returned type is a mapping from message names to message types.\n* If `MessageName` is a known `message`, the returned type is the inferred type of the given `MessageName`.\n* If `MessageName` is not a known `message`, the returned type is `never`.\n"
  },
  {
    "path": "examples/README.md",
    "content": "# protobuf-ts-types Examples\n\n## Usage\n\nOpen the directory of the example you want to run in VSCode, then run\n\n```\nnpm install\nnpm start\n```\n"
  },
  {
    "path": "examples/basic/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n.cache\n\n# vitepress build output\n**/.vitepress/dist\n\n# vitepress cache directory\n**/.vitepress/cache\n\n# Docusaurus cache and generated files\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n"
  },
  {
    "path": "examples/basic/index.ts",
    "content": "import { pbt } from \"../../src/index\";\n\nconst proto = `\n  syntax = \"proto3\";\n  \n  message Person {\n      string name = 1;\n      int32 id = 2;\n      bool is_ceo = 3;\n      optional string description = 4;\n  }\n\n  message Group {\n      string name = 1;\n      repeated Person people = 2;\n  }\n`;\n\ntype Proto = pbt.infer<typeof proto>;\ntype Person = Proto[\"Person\"];\ntype Group = Proto[\"Group\"];\n\nfunction greetPerson(person: Person) {\n  console.log(`Hello, ${person.name}!`);\n\n  if (person.description) {\n    console.log(`${person.description}`);\n  } else {\n    console.log(\"(no description)\");\n  }\n}\n\nfunction greetGroup(group: Group) {\n  console.log(`=========${\"=\".repeat(group.name.length)}===`);\n  console.log(`= Hello, ${group.name}! =`);\n  console.log(`=========${\"=\".repeat(group.name.length)}===`);\n\n  for (const person of group.people) {\n    greetPerson(person);\n    console.log();\n  }\n}\n\ngreetGroup({\n  name: \"Hooli\",\n  people: [\n    {\n      name: \"Gavin Belson\",\n      id: 0,\n      is_ceo: true,\n      description: \"CEO of Hooli\",\n    },\n    {\n      name: \"Richard Hendricks\",\n      id: 1,\n      is_ceo: true,\n      description: \"CEO of Pied Piper\",\n    },\n    {\n      name: \"Dinesh Chugtai\",\n      id: 2,\n      is_ceo: false,\n      description: \"Software Engineer\",\n    },\n    {\n      name: \"Jared Dunn\",\n      id: 3,\n      is_ceo: false,\n    },\n  ],\n});\n"
  },
  {
    "path": "examples/basic/package.json",
    "content": "{\n  \"name\": \"@protobuf-ts-types/example-basic\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.ts\",\n  \"private\": true,\n  \"scripts\": {\n    \"start\": \"tsx index.ts\"\n  },\n  \"author\": \"nathanhleung\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"\",\n  \"devDependencies\": {\n    \"tsx\": \"^4.19.3\",\n    \"typescript\": \"^5.8.3\"\n  }\n}\n"
  },
  {
    "path": "examples/basic/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n}\n\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"protobuf-ts-types\",\n  \"author\": \"nathanhleung\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Zero-codegen TypeScript `type` inference from protobuf `message`s\",\n  \"keywords\": [\n    \"protobuf\",\n    \"protobufjs\",\n    \"protobuf-es\",\n    \"proto\",\n    \"buf\",\n    \"typescript\",\n    \"type\",\n    \"inference\",\n    \"codegen\",\n    \"grpc\",\n    \"grpc-web\",\n    \"connect\"\n  ],\n  \"main\": \"src/index.ts\",\n  \"homepage\": \"https://github.com/nathanhleung/protobuf-ts-types#readme\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/nathanhleung/protobuf-ts-types.git\"\n  },\n  \"license\": \"Apache-2.0\",\n  \"devDependencies\": {\n    \"@types/node\": \"^22.14.1\",\n    \"typescript\": \"^5.8.3\"\n  },\n  \"dependencies\": {\n    \"type-fest\": \"^4.39.1\"\n  }\n}\n"
  },
  {
    "path": "src/array.ts",
    "content": "import type { Trim } from \"type-fest\";\n\n/**\n * Trims leading and trailing whitespace from each element of the given array\n * of strings.\n */\nexport type MapTrim<Strings extends string[]> = Strings extends [\n  infer Head,\n  ...infer Tail\n]\n  ? Head extends string\n    ? Tail extends string[]\n      ? [Trim<Head>, ...MapTrim<Tail>]\n      : []\n    : []\n  : [];\n\n/**\n * Removes empty strings from the given array of strings.\n */\nexport type FilterEmpty<Strings extends string[]> = Strings extends [\n  infer Head,\n  ...infer Tail\n]\n  ? Head extends \"\"\n    ? FilterEmpty<Tail extends string[] ? Tail : []>\n    : [Head, ...FilterEmpty<Tail extends string[] ? Tail : []>]\n  : [];\n\n/**\n * Given an array of objects, creates an object type where the keys are the\n * values of the given key for each object. Think of it as a combination of\n * lodash's [`keyBy`](https://lodash.com/docs/4.17.15#keyBy) and\n * [`pick`](https://lodash.com/docs/4.17.15#pick).\n */\nexport type KeyBy<\n  Objects extends { [key in Key]: string | number | symbol }[],\n  Key extends string\n> = {\n  [Object in Objects[number] as Object[Key]]: Object;\n};\n"
  },
  {
    "path": "src/index.ts",
    "content": "import type { infer as Infer } from \"./proto\";\n\nexport namespace pbt {\n  export type infer<\n    Proto extends string,\n    MessageName extends string = \"\"\n  > = Infer<Proto, MessageName>;\n}\n"
  },
  {
    "path": "src/proto.ts",
    "content": "import type { Split, Trim } from \"type-fest\";\nimport type { FilterEmpty, KeyBy, MapTrim } from \"./array\";\nimport type {\n  OptionalWhitespace,\n  StringToNumber,\n  Whitespace,\n  WrapWithNewlines,\n} from \"./string\";\n\n/**\n * Extracts message names from the given proto string.\n */\ntype MessageNames<Proto extends string> =\n  WrapWithNewlines<Proto> extends `${string}${Whitespace}message${Whitespace}${infer MessageName}${OptionalWhitespace}{${string}}${infer Rest}`\n    ? [MessageName, ...MessageNames<Rest>]\n    : [];\n\n/**\n * Extracts field definitions of the given message name.\n */\ntype RawFieldDefinitions<\n  Proto extends string,\n  MessageName extends MessageNames<Proto>[number]\n> = WrapWithNewlines<Proto> extends `${string}${Whitespace}message${Whitespace}${MessageName}${OptionalWhitespace}{${infer FieldDefinitions}}${string}`\n  ? FilterEmpty<MapTrim<Split<FieldDefinitions, \";\">>>\n  : [];\n\n/**\n * Given a raw field definition extracted by `RawFieldDefinitions`, parses it\n * into an object type.\n */\ntype ParseRawFieldDefinition<\n  Proto extends string,\n  RawFieldDefinition extends string\n> = RawFieldDefinition extends `${infer FieldCardinality}${Whitespace}${infer FieldType}${Whitespace}${infer FieldName}${OptionalWhitespace}=${OptionalWhitespace}${infer FieldNumber}`\n  ? FieldName extends \"\"\n    ? RawFieldDefinition extends `${infer FieldType}${Whitespace}${infer FieldName}${OptionalWhitespace}=${OptionalWhitespace}${infer FieldNumber}`\n      ? {\n          type: ParseFieldType<Proto, Trim<FieldType>>;\n          name: Trim<FieldName>;\n          number: StringToNumber<Trim<FieldNumber>>;\n        }\n      : never\n    : {\n        type: ParseFieldType<Proto, Trim<FieldType>, Trim<FieldCardinality>>;\n        name: Trim<FieldName>;\n        number: StringToNumber<Trim<FieldNumber>>;\n      }\n  : never;\n\n/**\n * Given a raw field type, parses it into a TypeScript type, handling\n * cardinality appropriately.\n */\ntype ParseFieldType<\n  Proto extends string,\n  RawFieldType extends string,\n  Cardinality extends string = \"\"\n> = Cardinality extends \"optional\"\n  ? ParseRawFieldType<Proto, RawFieldType> | undefined\n  : Cardinality extends \"repeated\"\n  ? ParseRawFieldType<Proto, RawFieldType>[]\n  : ParseRawFieldType<Proto, RawFieldType>;\n\n/**\n * Given a raw field type, parses it into a TypeScript type.\n */\ntype ParseRawFieldType<\n  Proto extends string,\n  RawType extends string\n> = RawType extends \"string\"\n  ? string\n  : RawType extends \"bool\"\n  ? boolean\n  : RawType extends \"bytes\"\n  ? Uint8Array\n  : RawType extends \"float\"\n  ? number\n  : RawType extends \"double\"\n  ? number\n  : RawType extends \"int32\"\n  ? number\n  : RawType extends \"int64\"\n  ? number\n  : RawType extends \"uint32\"\n  ? number\n  : RawType extends \"uint64\"\n  ? number\n  : MessagesByMessageName<Proto>[RawType];\n\n/**\n * Given raw field definitions extracted by `RawFieldDefinitions`, parses them\n * into object types using `ParseRawFieldDefinition`.\n */\ntype ParseRawFieldDefinitions<\n  Proto extends string,\n  RawDefinitions extends string[]\n> = RawDefinitions extends [infer Head, ...infer Tail]\n  ? Head extends string\n    ? Tail extends string[]\n      ? [\n          ParseRawFieldDefinition<Proto, Head>,\n          ...ParseRawFieldDefinitions<Proto, Tail>\n        ]\n      : []\n    : []\n  : [];\n\n/**\n * Extracts field definitions for a given message name.\n */\ntype FieldDefinitions<\n  Proto extends string,\n  MessageName extends MessageNames<Proto>[number]\n> = ParseRawFieldDefinitions<Proto, RawFieldDefinitions<Proto, MessageName>>;\n\n/**\n * Extracts a mapping of field names to field definitions for a given message\n * name.\n */\ntype FieldDefinitionsByFieldName<\n  Proto extends string,\n  MessageName extends MessageNames<Proto>[number]\n> = KeyBy<FieldDefinitions<Proto, MessageName>, \"name\">;\n\n/**\n * Extracts the field names for a given message name.\n */\ntype FieldNames<\n  Proto extends string,\n  MessageName extends MessageNames<Proto>[number]\n> = keyof FieldDefinitionsByFieldName<Proto, MessageName>;\n\n/**\n * Infers the type of the named message in the given proto string.\n */\ntype MessageType<\n  Proto extends string,\n  MessageName extends MessageNames<Proto>[number]\n> = {\n  [k in FieldNames<\n    Proto,\n    MessageName\n  > as undefined extends FieldDefinitionsByFieldName<\n    Proto,\n    MessageName\n  >[k][\"type\"]\n    ? never\n    : k]: FieldDefinitionsByFieldName<Proto, MessageName>[k][\"type\"];\n} & {\n  [k in FieldNames<\n    Proto,\n    MessageName\n  > as undefined extends FieldDefinitionsByFieldName<\n    Proto,\n    MessageName\n  >[k][\"type\"]\n    ? k\n    : never]?: FieldDefinitionsByFieldName<Proto, MessageName>[k][\"type\"];\n};\n\n/**\n * Creates a mapping of message names to their respective inferred types for\n * the given proto string.\n */\ntype MessagesByMessageName<Proto extends string> = {\n  [k in MessageNames<Proto>[number]]: MessageType<Proto, k>;\n};\n\nexport type infer<\n  Proto extends string,\n  MessageName extends string = \"\"\n> = MessageName extends \"\"\n  ? MessagesByMessageName<Proto>\n  : MessageName extends MessageNames<Proto>[number]\n  ? MessagesByMessageName<Proto>[MessageName]\n  : never;\n"
  },
  {
    "path": "src/string.ts",
    "content": "/** Wraps the given string type with newlines. */\nexport type WrapWithNewlines<T extends string> = `\\n${T}\\n`;\n\n/** A space, tab, newline, or carriage return. */\nexport type Whitespace = \" \" | \"\\t\" | \"\\n\" | \"\\r\";\n\nexport type OptionalWhitespace = \"\" | Whitespace;\n\nexport type StringToNumber<T extends string> =\n  T extends `${infer N extends number}` ? N : never;\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ES2024\"],\n    \"types\": [\"node\"],\n    \"target\": \"ES2024\",\n    \"allowJs\": false,\n    \"noEmit\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  }
]