Skip to Content

Generated Code Reference (ts-proto)

Detailed reference for the TypeScript code generated by protoc-gen-pothos with ts-proto runtime.

File Structure

For each .proto file, the plugin generates a .pb.pothos.ts file containing:

proto/example/user.proto src/__generated__/example/user.pb.pothos.ts

Each generated file includes:

  1. Import statements
  2. Object type definitions ($Ref)
  3. Input type definitions ($Shape, $Ref)
  4. Enum type definitions
  5. Union type definitions (for oneofs)

Naming Conventions

Export Identifiers

Proto DefinitionGenerated ExportType
message UserUser$RefObjectRef<User>
message User (input)UserInput$ShapeType alias
message User (input)UserInput$RefInputObjectRef<UserInput$Shape>
message User (partial)UserPartialInput$ShapeType alias
message User (partial)UserPartialInput$RefInputObjectRef<...>
enum RoleRole$RefEnumRef<Role, Role>
oneof content in MediaMediaContent$RefUnion type

Nested Messages

Nested messages use underscore-separated naming:

message User { message Address { string city = 1; } }

Generated exports:

  • UserAddress$Ref
  • UserAddressInput$Shape
  • UserAddressInput$Ref

Object Type Generation

Basic Object Type

message Date { uint32 year = 1; uint32 month = 2; uint32 day = 3; }
import { Date } from "@testapis/ts-proto/testapis/custom_types/date"; export const Date$Ref = builder.objectRef<Date>("Date"); builder.objectType(Date$Ref, { name: "Date", fields: (t) => ({ year: t.expose("year", { type: "Int", nullable: false, extensions: { protobufField: { name: "year", typeFullName: "uint32" } }, }), month: t.expose("month", { type: "Int", nullable: false, extensions: { protobufField: { name: "month", typeFullName: "uint32" } }, }), day: t.expose("day", { type: "Int", nullable: false, extensions: { protobufField: { name: "day", typeFullName: "uint32" } }, }), }), isTypeOf: (source) => { return (source as Date | { $type: string & {}; }).$type === "testapis.custom_types.Date"; }, extensions: { protobufMessage: { fullName: "testapis.custom_types.Date", name: "Date", package: "testapis.custom_types", }, }, });

Field Methods

MethodUsage
t.expose()Direct field exposure without resolver
t.field()Field with custom resolver

t.expose() is used when the field value can be accessed directly from the source object.

t.field() is used when any of the following conditions apply:

  • Bytes fields: Require Buffer conversion
  • Enum fields with unspecified/ignored values: Need resolver for value handling
  • Oneof fields: Access oneof member values
  • Squashed oneof union fields: Flatten nested oneof to parent type
  • Required nested message fields: Need non-null assertion (!)
  • Required repeated fields: Need non-null assertion for list

isTypeOf Implementation

ts-proto uses the $type property for type discrimination:

isTypeOf: (source) => { return (source as Message | { $type: string & {}; }).$type === "package.name.MessageName"; }

Empty Message Type

Messages with no fields generate a noop field to satisfy GraphQL schema requirements:

export const EmptyMessage$Ref = builder.objectRef<EmptyMessage>("EmptyMessage"); builder.objectType(EmptyMessage$Ref, { name: "EmptyMessage", fields: (t) => ({ _: t.field({ type: "Boolean", nullable: true, description: "noop field", resolve: () => true, }), }), isTypeOf: (source) => { return (source as EmptyMessage | { $type: string & {}; }).$type === "testapis.empty_types.EmptyMessage"; }, extensions: { protobufMessage: { fullName: "testapis.empty_types.EmptyMessage", name: "EmptyMessage", package: "testapis.empty_types", }, }, });

Input Type Generation

Basic Input Type

export type DateInput$Shape = { year: Date["year"]; month: Date["month"]; day: Date["day"]; }; export const DateInput$Ref: InputObjectRef<DateInput$Shape> = builder.inputRef< DateInput$Shape >("DateInput").implement({ fields: (t) => ({ year: t.field({ type: "Int", required: true, extensions: { protobufField: { name: "year", typeFullName: "uint32" } }, }), month: t.field({ type: "Int", required: true, extensions: { protobufField: { name: "month", typeFullName: "uint32" } }, }), day: t.field({ type: "Int", required: true, extensions: { protobufField: { name: "day", typeFullName: "uint32" } }, }), }), extensions: { protobufMessage: { fullName: "testapis.custom_types.Date", name: "Date", package: "testapis.custom_types", }, }, });

Shape Type

The $Shape type alias derives field types from the protobuf message type:

export type PostInput$Shape = { title: Post["title"]; // Required field publishedDate?: DateInput$Shape | null; // Optional nested message };

Partial Input Type

When partial_inputs=true is configured:

export type DatePartialInput$Shape = { year?: Date["year"] | null; month?: Date["month"] | null; day?: Date["day"] | null; }; export const DatePartialInput$Ref: InputObjectRef<DatePartialInput$Shape> = builder.inputRef<DatePartialInput$Shape>("DatePartialInput").implement({ fields: (t) => ({ year: t.field({ type: "Int", required: false, // All fields optional extensions: { protobufField: { name: "year", typeFullName: "uint32" } }, }), // ... }), // ... });

Enum Type Generation

Basic Enum

enum NotDeprecatedEnum { NOT_DEPRECATED_ENUM_UNSPECIFIED = 0; NOT_DEPRECATED_FOO = 1; DEPRECATED_BAR = 2 [deprecated = true]; }
export const NotDeprecatedEnum$Ref: EnumRef< NotDeprecatedEnum, NotDeprecatedEnum > = builder.enumType("NotDeprecatedEnum", { values: { NOT_DEPRECATED_FOO: { value: 1, extensions: { protobufEnumValue: { name: "NOT_DEPRECATED_FOO" } }, }, DEPRECATED_BAR: { deprecationReason: "testapis.deprecation.NotDeprecatedEnum.DEPRECATED_BAR is mark as deprecated in a *.proto file.", value: 2, extensions: { protobufEnumValue: { name: "DEPRECATED_BAR", options: { deprecated: true }, }, }, }, } as const, extensions: { protobufEnum: { name: "NotDeprecatedEnum", fullName: "testapis.deprecation.NotDeprecatedEnum", package: "testapis.deprecation", }, }, });

UNSPECIFIED Value Handling

Values matching <ENUM_NAME>_UNSPECIFIED at position 0 are excluded from GraphQL enum values.

When a field references an enum with an unspecified value, a resolver is generated:

nestedEnum: t.field({ type: ParentMessageNestedEnum$Ref, nullable: true, resolve: (source) => { if (source.nestedEnum === ParentMessage_NestedEnum.NESTED_ENUM_UNSPECIFIED) { return null; } return source.nestedEnum; }, // ... }),

IGNORED Value Handling

When an enum value is marked with the ignore option, the resolver throws an error:

prefixedEnum: t.field({ type: TestPrefixPrefixedEnum$Ref, nullable: true, resolve: (source) => { if (source.prefixedEnum === PrefixedEnum.PREFIXED_ENUM_UNSPECIFIED) { return null; } if (source.prefixedEnum === PrefixedEnum.PREFIXED_IGNORED) { throw new Error("PREFIXED_IGNORED is ignored in GraphQL schema"); } return source.prefixedEnum; }, // ... }),

Union Type Generation (Oneofs)

Basic Union

message OneofParent { oneof content { InnerMessage1 msg1 = 1; InnerMessage2 msg2 = 2; } }
export const OneofParentContent$Ref = builder.unionType( "OneofParentContent", { types: [InnerMessage1$Ref, InnerMessage2$Ref], extensions: { protobufOneof: { fullName: "testapis.oneof.OneofParent.content", name: "content", messageName: "OneofParent", package: "testapis.oneof", fields: [{ name: "msg1", type: "testapis.oneof.InnerMessage1", }, { name: "msg2", type: "testapis.oneof.InnerMessage2", }], }, }, }, );

Oneof Field Resolver

ts-proto accesses oneof members directly on the source object:

content: t.field({ type: OneofParentContent$Ref, nullable: true, resolve: (source) => { const value = source.msg1 ?? source.msg2; if (value == null) { return null; } return value; }, extensions: { protobufField: { name: "content" } }, }),

Required Oneof

When marked with // Required.:

requiredOneofMembers: t.field({ type: OneofParentRequiredOneofMembers$Ref, nullable: false, description: "Required. disallow not_set.", resolve: (source) => { const value = source.requiredMessage1 ?? source.requiredMessage2; if (value == null) { throw new Error("requiredOneofMembers should not be null"); } return value; }, extensions: { protobufField: { name: "required_oneof_members" } }, }),

Squashed Oneof Union

When a message contains only a oneof field and is marked with squashUnion: true option:

message PrefixedMessage { message SquashedMessage { oneof content { option (graphql.oneof).squash_union = true; InnerMessage oneof_field = 1 [(graphql.object_type).squash_union = true]; InnerMessage2 oneof_field_2 = 2 [(graphql.object_type).squash_union = true]; } } SquashedMessage squashed_message = 5; }

Generated union type:

export const TestPrefixPrefixedMessageSquashedMessage$Ref = builder.unionType( "TestPrefixPrefixedMessageSquashedMessage", { types: [ TestPrefixPrefixedMessageInnerMessage$Ref, TestPrefixPrefixedMessageInnerMessage2$Ref, ], extensions: { protobufOneof: { fullName: "testapis.extensions.PrefixedMessage.SquashedMessage", name: "SquashedMessage", package: "testapis.extensions", fields: [{ name: "oneof_field", type: "testapis.extensions.PrefixedMessage.InnerMessage", options: { "[graphql.object_type]": { squashUnion: true } }, }, { name: "oneof_field_2", type: "testapis.extensions.PrefixedMessage.InnerMessage2", options: { "[graphql.object_type]": { squashUnion: true } }, }], }, }, }, );

Generated field resolver:

squashedMessage: t.field({ type: TestPrefixPrefixedMessageSquashedMessage$Ref, nullable: true, resolve: (source) => { const value = source.squashedMessage?.oneofField ?? source.squashedMessage?.oneofField2; if (value == null) { return null; } return value; }, extensions: { protobufField: { name: "squashed_message", typeFullName: "testapis.extensions.PrefixedMessage.SquashedMessage", }, }, }),

Repeated squashed oneof field:

squashedMessages: t.field({ type: [TestPrefixPrefixedMessageSquashedMessage$Ref], nullable: { list: true, items: false }, resolve: (source) => { return source.squashedMessages.map((item) => { const value = item?.oneofField ?? item?.oneofField2; if (value == null) { throw new Error("squashedMessages should not be null"); } return value; }); }, extensions: { protobufField: { name: "squashed_messages", typeFullName: "testapis.extensions.PrefixedMessage.SquashedMessage", }, }, }),

Field Resolver Patterns

Bytes Field

Bytes fields require Buffer conversion:

data: t.field({ type: "Byte", nullable: true, resolve: (source) => { return source.data == null ? null : Buffer.from(source.data); }, }),

Repeated Bytes Field

dataList: t.field({ type: ["Byte"], nullable: { list: false, items: false }, resolve: (source) => { return source.dataList.map((v) => Buffer.from(v)); }, }),

Nested Message Field

Optional nested message fields use t.expose():

optionalPrimitives: t.expose("optionalPrimitives", { type: Primitives$Ref, nullable: true, description: "Optional.", extensions: { protobufField: { name: "optional_primitives", typeFullName: "testapis.primitives.Primitives", }, }, }),

Required Nested Message Field

Required nested message fields use t.field() with non-null assertion:

requiredPrimitives: t.field({ type: Primitives$Ref, nullable: false, description: "Required.", resolve: (source) => { return source.requiredPrimitives!; }, extensions: { protobufField: { name: "required_primitives", typeFullName: "testapis.primitives.Primitives", }, }, }),

Repeated Field Nullability

For list (repeated) fields, the nullable property uses an object structure:

// Required list - list and items are never null requiredPrimitivesList: t.field({ type: [Primitives$Ref], nullable: { list: false, items: false }, description: "Required.", resolve: (source) => { return source.requiredPrimitivesList!; }, extensions: { protobufField: { name: "required_primitives_list", typeFullName: "testapis.primitives.Primitives", }, }, }), // Optional list - list can be null, but items are never null optionalPrimitivesList: t.expose("optionalPrimitivesList", { type: [Primitives$Ref], nullable: { list: true, items: false }, description: "Optional.", extensions: { protobufField: { name: "optional_primitives_list", typeFullName: "testapis.primitives.Primitives", }, }, }),
Nullable ValueGraphQL Type
{ list: false, items: false }[Type!]!
{ list: true, items: false }[Type!]

64-bit Integer Fields

ts-proto represents 64-bit integers as String:

requiredInt64Value: t.expose("requiredInt64Value", { type: "String", nullable: false, extensions: { protobufField: { name: "required_int64_value", typeFullName: "int64" }, }, }),

Extensions Object

Generated code includes extensions objects for introspection:

protobufMessage

extensions: { protobufMessage: { fullName: "testapis.custom_types.Date", name: "Date", package: "testapis.custom_types", }, }

protobufField

extensions: { protobufField: { name: "published_date", typeFullName: "testapis.custom_types.Date", }, }

protobufEnum

extensions: { protobufEnum: { name: "MyEnum", fullName: "testapi.enums.MyEnum", package: "testapi.enums", }, }

protobufEnumValue

extensions: { protobufEnumValue: { name: "MY_ENUM_FOO", options: { deprecated: true }, // if applicable }, }

protobufOneof

extensions: { protobufOneof: { fullName: "testapis.oneof.Media.content", name: "content", messageName: "Media", package: "testapis.oneof", fields: [ { name: "image", type: "testapis.oneof.Image" }, { name: "video", type: "testapis.oneof.Video" }, ], }, }