When you write api.update_user.useMutation(...), two systems cooperate. The compiler already knows the hook exists (the type channel), but the running program must actually produce that hook. That production happens in a Proxy get trap, reading the runtime kinds map.
The import
import { procedureKinds as generatedKinds } from '@pyrpc/types';This is the value import that made v0.12.0 a runtime-module release. It is not import type, the adapter needs the actual object, so the bundler must resolve @pyrpc/types to the generated file. The alias, the node_modules gap, the throwing placeholder: all of it exists to make this one line resolve correctly.
The override seam
const { kinds: kindsOverride, ...clientOptions } = options;
const kinds = (kindsOverride ?? generatedKinds) as ProcedureKindMap<TProcedures>;The adapter reads codegen output by default but lets a caller inject kinds explicitly. This is the testability valve: in a unit test with no generated module in sight, the test passes a literal kinds map and never touches the Proxy.
The Proxy that assembles hooks
return new Proxy(root as object, {
get(target, prop: string | symbol) {
if (typeof prop !== 'string') return undefined;
if (prop in target) return target[prop];
const kind = kinds?.[prop] as 'query' | 'mutation' | undefined;
return createProcedureHooks(client, prop, kind);
},
});Property access on api is intercepted. Real members (Provider, useUtils, client) short-circuit. Everything else is treated as a procedure name, its kind is looked up in the runtime map, and a hook bundle is created on demand.
The kind decides the hooks
function createProcedureHooks(client, procedure, kind) {
if (kind !== 'mutation') {
hooks.useQuery = (input, options) => useQuery({
...options,
queryKey: getProcedureQueryKey(procedure, input),
queryFn: () => callProcedure(fn, input),
});
}
if (kind !== 'query') {
hooks.useMutation = (options) => useMutation({
...options,
mutationFn: (input) => callProcedure(fn, input),
});
}
return hooks;
}The comparisons are deliberately inverted. A mutation kind suppresses useQuery (kind !== 'mutation' is false); a query kind suppresses useMutation. And undefined (the unknown or unkinded case) passes both guards, yielding both hooks. The runtime default is permissive, matching the type-level undefined fallback.
The single point of truth
Notice the call flow: queryKey and callProcedure are shared between both hook kinds. The only difference between query and mutation hooks is which TanStack Query hook wraps the same underlying call, which is exactly the kind knowledge the generated module supplies. The runtime channel's whole job is one lookup per procedure access: kinds?.[prop].

pyRPC