Jun-14, 2025 · 5min
最近在写一个 ai 集成应用,起初用的 tRPC 和 ai-sdk 来实现,但是发现 tRPC 无法很好的与 ai-sdk 结合,其返回的类型必须要经过其内部的序列化,无法单独的返回 ai-sdk 返回的 response 类型
最终选择了 ts-rest,可以非常灵活的定义返回的类型,而且可以与 ai-sdk 很好的结合。完美的实现我的需求
ts-rest 推荐使用的是 react-query,但是个人比较喜欢的还是 swr 比较的简单
下面是我最初的使用方式
const values = useSWR(
data?.user.id ? [contract.getCredits.path, data?.user.id] : null,
async ([_, id]) => {
const res = await api.getCredits({
query: {
userId: id,
},
})
if (res.status !== 200) {
return null
}
return res.body
},
)
可以发现非常的繁琐,比如这个响应在非 200 的情况下,几乎每个请求都要做类似的处理
参考了 tRPC 与 React Query 的集成方案 trpc-with-react-query 最终实现的效果类似这样
const values = api.getCredits.useSWR(
{
query: {
userId: data?.user.id || '',
},
},
{
enabled: !!data?.user.id,
},
)
返回的 values
的 data
只有 200
时的类型,可以通过 enabled
控制时候发送请求,整体的实现也比较简单,类型推导方面可能会比较的复杂,下面是具体的实现,参考了 ts-rest 的 initClient 的实现
import {
getRouteQuery,
initClient,
isAppRoute,
isAppRouteQuery,
} from '@ts-rest/core'
import useSWR from 'swr'
import useSWRMutation from 'swr/mutation'
import type {
AppRoute,
AppRouteDeleteNoBody,
AppRouteMutation,
AppRouteQuery,
AppRouter,
AreAllPropertiesOptional,
ClientArgs,
ClientInferRequest,
ClientInferResponseBody,
ClientInferResponses,
InitClientArgs,
PartialClientInferRequest,
Prettify,
} from '@ts-rest/core'
import type { SWRConfiguration, SWRResponse } from 'swr'
import type {
SWRMutationConfiguration,
SWRMutationResponse,
} from 'swr/mutation'
function getSWRRouteQuery(route: AppRouteQuery, clientArgs: InitClientArgs) {
const queryFn = getRouteQuery(route, clientArgs)
return {
useSWR: (
args: ClientInferRequest<AppRouteMutation, ClientArgs>,
options: {
enabled?: boolean
} & SWRConfiguration = {},
) => {
const { enabled = true, ...SWROptions } = options
const values = useSWR(
enabled ? [route.path, args] : null,
async () => {
const res = await queryFn(args)
if (res.status !== 200) {
throw new Error('error')
}
return res.body
},
SWROptions,
)
return values
},
}
}
function getSWRRouteMutation(
route: AppRouteMutation | AppRouteDeleteNoBody,
clientArgs: InitClientArgs,
) {
const mutationFn = getRouteQuery(route, clientArgs)
return {
useSWRMutation: (options: SWRMutationConfiguration<any, any, any> = {}) => {
const values = useSWRMutation(
[route.path],
async (_url: string, { arg }: { arg: any }) => {
const res = await mutationFn(arg)
if (res.status !== 200) {
throw new Error('error')
}
return res.body
},
options,
)
return values
},
}
}
type AppSWRRouteFunction<
TRoute extends AppRoute,
TClientArgs extends ClientArgs,
TArgs = PartialClientInferRequest<TRoute, TClientArgs>,
> =
AreAllPropertiesOptional<TArgs> extends true
? (
args?: Prettify<TArgs>,
) => Promise<Prettify<ClientInferResponses<TRoute>>>
: TRoute extends AppRouteQuery
? {
useSWR: <Data = Prettify<ClientInferResponseBody<TRoute, 200>>>(
args?: Prettify<TArgs>,
options?: {
enabled?: boolean
} & SWRConfiguration<Data>,
) => SWRResponse<Data>
}
: TRoute extends AppRouteMutation | AppRouteDeleteNoBody
? {
useSWRMutation: <
Data = Prettify<ClientInferResponseBody<TRoute, 200>>,
ExtraArg = Prettify<TArgs>,
>(
options?: SWRMutationConfiguration<Data, any, any, ExtraArg>,
) => SWRMutationResponse<Data, any, any, ExtraArg>
}
: (
args: Prettify<TArgs>,
) => Promise<Prettify<ClientInferResponses<TRoute>>>
type RecursiveProxyObj<T extends AppRouter, TClientArgs extends ClientArgs> = {
[TKey in keyof T]: T[TKey] extends AppRoute
? AppSWRRouteFunction<T[TKey], TClientArgs>
: T[TKey] extends AppRouter
? RecursiveProxyObj<T[TKey], TClientArgs>
: never
}
export type InitClientReturn<
T extends AppRouter,
TClientArgs extends ClientArgs,
> = RecursiveProxyObj<T, TClientArgs>
export function createApi<
T extends AppRouter,
TClientArgs extends InitClientArgs,
>(router: T, args: TClientArgs): InitClientReturn<T, TClientArgs> {
const api = Object.fromEntries(
Object.entries(router).map(([key, subRouter]) => {
if (isAppRoute(subRouter)) {
if (isAppRouteQuery(subRouter)) {
return [key, getSWRRouteQuery(subRouter, args)]
}
return [key, getSWRRouteMutation(subRouter, args)]
} else {
return [key, initClient(subRouter, args)]
}
}),
)
return api
}
export const api = createApi(contract, {
baseUrl: '.',
baseHeaders: {},
throwOnUnknownStatus: true,
})
以上的实现可以自由的在 getSWRRouteQuery
和 getSWRRouteMutation
中添加中间件,比如添加错误处理并全局提示等