FluX
FluX
TtRPC
Created by FluX on 12/4/2024 in #❓-help
Infer TRoot and TProcedure for specific procedures (polymorphism)
Hello! I'm trying to build a custom hook that returns specific procedures of a router:
"use client"

import { MutationLike, QueryLike } from "@trpc/react-query/shared"
import { AnyProcedure, AnyRootTypes } from "@trpc/server/unstable-core-do-not-import"
import { trpc } from "@/trpc/client"

type RequiredProcedures<TRoot extends AnyRootTypes, TProcedure extends AnyProcedure> = {
create: MutationLike<TRoot, TProcedure>
edit: MutationLike<TRoot, TProcedure>
list: QueryLike<TRoot, TProcedure>
details: QueryLike<TRoot, TProcedure>
}

export function useApi<T extends RequiredProcedures<any, any>>(
getRouter: (
router: typeof trpc
) => T extends RequiredProcedures<infer TRoot, infer TProcedure> ? RequiredProcedures<TRoot, TProcedure> : never
) {
const router = getRouter(trpc)

return {
create: router.create,
edit: router.edit,
list: router.list,
details: router.details,
}
}

function Component() {
const { create, edit } = useApi((router) => router.admin.product)

const mut = create.useMutation()

async function handleCreate() {
await mut.mutateAsync({})
}
}
"use client"

import { MutationLike, QueryLike } from "@trpc/react-query/shared"
import { AnyProcedure, AnyRootTypes } from "@trpc/server/unstable-core-do-not-import"
import { trpc } from "@/trpc/client"

type RequiredProcedures<TRoot extends AnyRootTypes, TProcedure extends AnyProcedure> = {
create: MutationLike<TRoot, TProcedure>
edit: MutationLike<TRoot, TProcedure>
list: QueryLike<TRoot, TProcedure>
details: QueryLike<TRoot, TProcedure>
}

export function useApi<T extends RequiredProcedures<any, any>>(
getRouter: (
router: typeof trpc
) => T extends RequiredProcedures<infer TRoot, infer TProcedure> ? RequiredProcedures<TRoot, TProcedure> : never
) {
const router = getRouter(trpc)

return {
create: router.create,
edit: router.edit,
list: router.list,
details: router.details,
}
}

function Component() {
const { create, edit } = useApi((router) => router.admin.product)

const mut = create.useMutation()

async function handleCreate() {
await mut.mutateAsync({})
}
}
How can I infer TRoot and TProcedure? Currently it is typed as any in T extends RequiredProcedures<any, any>. But of course now my input and output types are also any, which I don't want. Any help is appreciated! :) Thanks!
2 replies
TtRPC
Created by FluX on 11/28/2024 in #❓-help
Possible to build this custom hook?
Hello! I'm facing a challenge and hope to get some guidance here. I'm building an admin dashboard for creating and editing data - let's just say it's product data. I built a product management form that I want to use for both creating and editing a product, because the fields are the same. I'd like to build a custom hook which returns multiple useQuerys or useMutations for a tRPC route. The hook might look like this:
// single query/mutation
const mutation = useApi((router) => router.admin.product.edit)
const query = useApi((router) => router.public.product.details)
// or multiple hooks for a router
const { editMutation, createMutation } = useApi((router) => router.admin.product)
// ^
// | with type error, if tRPC route (e.g. `admin.product.edit`) does not exist
// single query/mutation
const mutation = useApi((router) => router.admin.product.edit)
const query = useApi((router) => router.public.product.details)
// or multiple hooks for a router
const { editMutation, createMutation } = useApi((router) => router.admin.product)
// ^
// | with type error, if tRPC route (e.g. `admin.product.edit`) does not exist
I've tried creating such a hook but it's not very great and absolutely not type-safe. Any idea how this could be properly implemented? Would appreciate any help :)
export const useManager = <FormData extends FieldValues>(
router: keyof AppRouter["admin"],
dataMode: "create" | "edit"
) => {
const nextRouter = useRouter()

function getMutation(): UseTRPCMutationResult<FormData, any, any, any> {
if (dataMode === "create") {
// @ts-ignore
return trpc.admin[router].create.useMutation()
} else {
// @ts-ignore
return trpc.admin[router].edit.useMutation()
}
}

const mutation = getMutation()

async function submit(data: FormData) {
return await mutation.mutateAsync(data) as FormData
}

// @ts-ignore
const deleteMutation: UseTRPCMutationResult<FormData, any, any, any> = trpc.admin[router].delete.useMutation()

async function purge(slug: string) {
return await deleteMutation.mutateAsync({ slug })
}

const isLoading = mutation.isPending || deleteMutation.isPending

return { submit, isLoading, redirect: nextRouter.push, purge }
}
export const useManager = <FormData extends FieldValues>(
router: keyof AppRouter["admin"],
dataMode: "create" | "edit"
) => {
const nextRouter = useRouter()

function getMutation(): UseTRPCMutationResult<FormData, any, any, any> {
if (dataMode === "create") {
// @ts-ignore
return trpc.admin[router].create.useMutation()
} else {
// @ts-ignore
return trpc.admin[router].edit.useMutation()
}
}

const mutation = getMutation()

async function submit(data: FormData) {
return await mutation.mutateAsync(data) as FormData
}

// @ts-ignore
const deleteMutation: UseTRPCMutationResult<FormData, any, any, any> = trpc.admin[router].delete.useMutation()

async function purge(slug: string) {
return await deleteMutation.mutateAsync({ slug })
}

const isLoading = mutation.isPending || deleteMutation.isPending

return { submit, isLoading, redirect: nextRouter.push, purge }
}
4 replies