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.tsEach generated file includes:
- Import statements
- Object type definitions (
$Ref) - Input type definitions (
$Shape,$Ref) - Enum type definitions
- Union type definitions (for oneofs)
Naming Conventions
Export Identifiers
| Proto Definition | Generated Export | Type |
|---|---|---|
message User | User$Ref | ObjectRef<User> |
message User (input) | UserInput$Shape | Type alias |
message User (input) | UserInput$Ref | InputObjectRef<UserInput$Shape> |
message User (partial) | UserPartialInput$Shape | Type alias |
message User (partial) | UserPartialInput$Ref | InputObjectRef<...> |
enum Role | Role$Ref | EnumRef<Role, Role> |
oneof content in Media | MediaContent$Ref | Union type |
Nested Messages
Nested messages use underscore-separated naming:
message User {
message Address {
string city = 1;
}
}Generated exports:
UserAddress$RefUserAddressInput$ShapeUserAddressInput$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
| Method | Usage |
|---|---|
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
Bufferconversion - 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 Value | GraphQL 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" },
],
},
}