Skip to content

Code Generation

Generation turns Go procedure registrations into the TypeScript contract consumed by tRPC clients.

Use static analysis before building the frontend:

//go:generate go tool trpcgo generate -o ../web/gen/trpc.ts --zod ../web/gen/zod.ts ./...
Terminal window
go generate ./...

Static analysis reads source packages with go/packages, so it can preserve source-only information that reflection cannot see.

Terminal window
go tool trpcgo generate [flags] [packages]

If no package pattern is provided, trpcgo analyzes ..

FlagDescription
-o, -outputTypeScript output file. Defaults to stdout.
-dirWorking directory for package resolution. Defaults to ..
-w, -watchWatch Go files and regenerate on write/create events.
-zodZod schema output file.
-zod-miniEmit zod/mini functional syntax.
-enumsRuntime enum value object output file.

Examples:

Terminal window
go tool trpcgo generate -o web/gen/trpc.ts ./...
go tool trpcgo generate -o web/gen/trpc.ts --zod web/gen/zod.ts ./...
go tool trpcgo generate -o web/gen/trpc.ts --enums web/gen/enums.ts ./...
go tool trpcgo generate -o web/gen/trpc.ts --zod web/gen/zod.ts -w ./...

The router can write generated files from registered procedure reflection types:

if err := router.GenerateTS("web/gen/trpc.ts"); err != nil {
return err
}
if err := router.GenerateZod("web/gen/zod.ts"); err != nil {
return err
}

In normal development, prefer the integrated watcher:

router := trpcgo.NewRouter(
trpcgo.WithDev(true),
trpcgo.WithTypeOutput("../web/gen/trpc.ts"),
trpcgo.WithZodOutput("../web/gen/zod.ts"),
trpcgo.WithEnumsOutput("../web/gen/enums.ts"),
)
defer router.Close()

When trpc.NewHandler is constructed, trpcgo starts the watcher. It generates once from source, then regenerates when .go files change. If source analysis fails because the Go code is temporarily broken, previous generated files are preserved.

The watcher runs static analysis, so it resolves const unions and the runtime enum value objects (WithEnumsOutput) at full fidelity — the same as the CLI. The reflection methods GenerateTS and GenerateZod cannot see Go const groups, so there is deliberately no GenerateEnums counterpart: runtime enum value objects come only from WithEnumsOutput under the watcher or --enums on the CLI.

Use WithWatchPackages to avoid watching unrelated frontend or generated directories in larger repositories:

trpcgo.WithWatchPackages("./cmd/api", "./internal/...")
FeatureStatic CLIRuntime reflection
Registered procedure input/output typesYesYes
json, tstype, validate, ts_doc, zod_omit tagsYesYes
Go doc comments as JSDocYesNo
const groups as string/number unionsYesNo
runtime enum value objects (enums.ts)YesNo
aliases and defined basic typesYesLimited
source-level typed output parser discoveryYesRegistered typed parsers only

Const groups generate unions for reachable named types declared in the analyzed packages or other packages in the same Go module. Standard-library and third-party constants are ignored, so types like time.Duration still generate as their normal primitive TypeScript shape.

Use the CLI for generated files committed or built in CI. Use dev watch for a fast local feedback loop.

Generated trpc.ts includes:

  • // Code generated by trpcgo. DO NOT EDIT.
  • Type-only imports from @trpc/server.
  • Exported TypeScript definitions for reachable Go types.
  • $Query, $Mutation, and $Subscription helper aliases only when needed.
  • Nested AppRouterRecord generated from dot-separated procedure paths.
  • Exported AppRouter.
  • RouterInputs and RouterOutputs helpers when procedures exist.

Procedure paths become nested objects:

trpcgo.MustQuery(router, "user.get", getUser)
trpcgo.MustMutation(router, "admin.user.ban", banUser)
type AppRouterRecord = {
user: {
get: $Query<GetUserInput, User>;
};
admin: {
user: {
ban: $Mutation<BanUserInput, BanUserResult>;
};
};
};

Common Go-to-TypeScript mappings:

GoTypeScript
stringstring
boolboolean
numeric typesnumber
time.Timestring
[]T, [N]TT[]
[]bytestring
map[K]VRecord<K, V>
any, interface{}unknown
json.RawMessageunknown
json.Numbernumber
TrackedEvent[T]T

Pointer fields and fields tagged omitempty or omitzero become optional unless overridden with tstype:",required".

Static generation discovers calls to the top-level registration functions such as Query, Mutation, Subscribe, SubscribeWithFinal, and their Void/Must variants.

Important limits:

  • Procedure paths must be string literals for static analysis.
  • Packages must load and type-check successfully.
  • Custom wrapper functions are only detected if the analyzer can see the underlying top-level registration call with a literal path.
  • Zod generation targets procedure input types and their dependencies, not output-only types.
  • Reflection generation cannot emit source comments, const unions, or the runtime enum value objects derived from them. These need source analysis — the CLI or the dev watcher.

Pass --zod or configure WithZodOutput to generate schemas for typed procedure inputs.

Terminal window
go tool trpcgo generate -o web/gen/trpc.ts --zod web/gen/zod.ts ./...

If no procedures have typed inputs, runtime GenerateZod and dev watch remove stale Zod files. The CLI can still create an empty file.

See Zod Schemas for validate tag mapping, zod/mini, omitempty, dive, cross-field rules, and frontend usage.

A TypeScript string-literal union is erased at runtime, so iterating an enum’s members, populating a <select>, or writing a membership guard means re-listing the values by hand — which silently drifts when the Go enum gains a member. Pass --enums or configure WithEnumsOutput to also emit the members as a runtime as const object:

Terminal window
go tool trpcgo generate -o web/gen/trpc.ts --enums web/gen/enums.ts ./...
// enums.ts — Code generated by trpcgo. DO NOT EDIT.
export const RoleEnum = {
viewer: "viewer",
admin: "admin",
owner: "owner",
} as const;

The object is keyed by value, so Object.values(RoleEnum) iterates the members, Object.hasOwn(RoleEnum, value) tests membership, and RoleEnum.viewer autocompletes. Members come from the same analysis as the Role union, so they cannot drift from the type.

Enum objects are written to their own file to keep trpc.ts free of runtime values (type-only imports). They cover every named string enum, including output-only enums that never appear in a Zod input schema. Numeric enums are skipped.

If --enums or WithEnumsOutput is set but no named string enums are in scope, trpcgo still writes a generated header-only enums.ts. The CLI and dev watcher intentionally match here; unlike Zod output, the watcher does not delete an empty enum file.

Like const-union narrowing, enum objects come from source analysis, so they are produced by the CLI (--enums, including -w) and the dev watcher (WithEnumsOutput with WithDev) — not by the reflection-based GenerateTS/GenerateZod, which never see Go const groups.