← Back to Blog

ProcedureHooksForKind: the type-level mirror

·7 min read

The runtime Proxy reads kinds to decide what exists. But TypeScript must agree before your code even compiles, and it does, through a conditional type that mirrors the runtime selection one-for-one.

The three hook shapes

The adapter defines three building blocks, each a mapped object type wrapping a TanStack Query hook:

export type ProcedureQueryHooks<TProc> = {
  useQuery: <TData = ProcResult<TProc>>(
    input: QueryInput<TProc>,
    options?: Omit<UseQueryOptions<ProcResult<TProc>, Error, TData>, 'queryKey' | 'queryFn'>,
  ) => UseQueryResult<TData, Error>;
};

export type ProcedureMutationHooks<TProc> = {
  useMutation: <TContext = unknown>(
    options?: Omit<UseMutationOptions<ProcResult<TProc>, Error, QueryInput<TProc>, TContext>, 'mutationFn'>,
  ) => UseMutationResult<ProcResult<TProc>, Error, QueryInput<TProc>, TContext>;
};

export type ProcedureHooksBoth<TProc> = ProcedureQueryHooks<TProc> &
  ProcedureMutationHooks<TProc>;

Note how the query and mutation inputs differ. useQuery takes the input as its first argument and returns a result that may be wider than the procedure's (<TData>). useMutation takes only options and returns a result plus the imperative mutate. The two contracts are genuinely different, which is why exposing both on a mutation-only procedure is a real API leak, not a cosmetic one.

The selector

export type ProcedureHooksForKind<
  TProc extends AnyProc,
  TKind extends ProcedureKind | undefined,
> = TKind extends 'mutation'
  ? ProcedureMutationHooks<TProc>
  : TKind extends 'query'
    ? ProcedureQueryHooks<TProc>
    : ProcedureHooksBoth<TProc>;

This is the type-level twin of the runtime guard. Where the Proxy asks "is the kind mutation?", this conditional type asks the same question of a type parameter. The order matters: 'mutation' first, then 'query', then the undefined fallback of both.

Wired into the client shape

export type ReactClient<TProcedures, TKinds = {}> = {
  [K in keyof TProcedures]: ProcedureHooksForKind<TProcedures[K], TKinds[K]>;
} & {
  Provider: ComponentType<ReactClientProviderProps>;
  useUtils: () => ReactClientUtils<TProcedures>;
  client: TProcedures;
};

A mapped type walks every procedure key and applies ProcedureHooksForKind with that procedure's inferred kind. The intersection adds the Provider/useUtils/client members, matching exactly which prop in target checks the Proxy short-circuits at runtime.

Mirror discipline

The striking thing is how precisely the runtime and type systems parallel each other:

  • Runtime: kind !== 'mutation' → include useQuery
  • Type: TKind extends 'mutation' → exclude useQuery

Same decision, two languages. Keeping the two mirrors in step is a discipline the codegen contract makes possible: because kinds come from a single generated artifact, the runtime branch and the type branch can never disagree about which hooks a procedure supports.