TRPC类型推理的限制以及我们如何改进它
#javascript #网络开发人员 #typescript #trpc

TRPC尽管历史悠久,但在Node.js/TypeScript社区中却广受欢迎。快速采用的主要原因之一来自其出色的轻加权设计 - 没有图谱可以编写,没有发电机可以运行。一切都神奇地奏效,利用打字稿的强大类型推理能力。它是提供最佳开发人员体验的API工具包之一。

但是,其功率也受到类型推理能力的上限的限制。让我们看一个例子。假设我有一个后端服务功能,该功能获取了一篇博客文章,其签名如下:

export type Post = { id: number; title: string; }

export type User = { id: number; name: string; }

export type LoadPostArgs = {
    id: number;
    withAuthor?: boolean;
};

export type LoadPostResult<T extends LoadPostArgs> =
    T['withAuthor'] extends true ? Post & { author: User } : Post;

export function loadPost<T extends LoadPostArgs>(args: T): LoadPostResult<T> {
    ...
}

此通用函数的独特之处在于,其返回类型“适应”输入类型:

// p1 is typed `Post`
const p1 = loadPost({id: 1});

// p2 is typed `Post & { author: User }`
const p2 = loadPost({id: 1, withAuthor: true});

这种动态打字可带来令人愉悦的自动完成体验,并有助于在编译时捕获错误。

让S通过TRPC路由器公开此功能:

// routers.ts
const appRouter = router({
    loadPost: publicProcedure
        .input(z.object({ id: z.number(), withAuthor: z.boolean().optional() }))
        .query(({ input }) => loadPost(input)),
});

export type AppRouter = typeof appRouter;

然后从客户端消耗它:

const trpc = createTRPCProxyClient<AppRouter>({...});

const p1 = await trpc.loadPost.query({ id: 1 });
const p2 = await trpc.loadPost.query({ id: 1, withAuthor: true });

p1p2均为Post。动态性丢失了。

为什么会发生?

让我们再次查看通用功能:

export function loadPost<T extends LoadPostArgs>(args: T): LoadPostResult<T> {
    ...
}

被调用时,从混凝土输入参数的类型中推断出通用类型参数T(只要满足loadPostargs类型)即可。之后,打字稿编译器可以基于推断的T进一步推断返回类型。关键是一切都发生在函数调用的上下文中。

尽管TRPC在调用远程API时给出了简单函数调用的错觉,但其情况却大不相同。在服务器端路由器注册期间,从ZOD模式对输入的形状进行静态分析,并且无法定义“通用”路由器,您可以在客户端上使用混凝土类型进行实例化。

要进行这种“动态”通用打字工作,TRPC需要能够在内部内部持有“非验证”的通用功能类型,并在不同的上下文中实例化。这需要一个名为“高态类型”的语言功能尚未实施的语言功能。实际上,该功能请求是在2014年创建的,我们可以庆祝其10年周年纪念日ð。

Allow classes to be parametric in other parametric classes #1213

metaweta avatar
metaweta 发布在

这是允许仿制药作为类型参数的建议。目前可以编写单调的特定示例,但是为了编写所有monads所满足的界面,我建议写作

在class =“ pl-v”> t <>>> { 地图 < a b > f a a => b t < a > = = > t < b > ; 提升 < a > a a t < a > ; 在pl-c1“ >> tta t < t < a >>> t < a > ; }

同样,可以编写笛卡尔函数的特定示例,但是为了编写所有笛卡尔函子所满足的界面,我提出写作

在class =“ pl-v”> t <>>> { asl < a > (a:array < t < a > >在 array < a >> ; }

参数类型参数可以接受任意数量的参数:

在class =“ pl-v”> t <>> { bar < a b > f a a => b t < a b > ; }

也就是说,当类型参数之后是tilde和自然差异时,应允许将类型参数用作声明其余部分中给定ARITY的通用类型。

现在就像现在的情况一样,在实现此类接口时,应填充通用类型参数:

在class =“ pl-ent”> a > 实现monad < array > { 在pl-kos“>, b > f a a => b array < a > = = > array < b > { < “ pl-v”> array < a > => arr map f ; } 提升 < a > a a array < a > { return [ a ] ; } 在pl-c1“ >> tta array < array < a >>> array < a > { 返回 tta 降低 prev cur => prev concat cur ; } }

除了直接允许参数中的通用类型的组成外,我还建议Typedef还支持以这种方式定义通用物(请参阅issue 3086):

  typedef  也许  <  array   <>>>  loadPost'> & {
        loadPost: {
            query: <T extends Parameters<LoadPostFn>[0]>(
                input: T
            ) => Promise<ReturnType<LoadPostFn<T>>>;
        };
    };
}

现在客户端键入都很好:

const trpc = createMyTRPCProxyClient({ ... });

// post is typed as `Post & { author: User }`
const post = await trpc.post.findFirst.query({
    where: { id: postId }, 
    include: { author: true }
);

类型推导与代码生成

类型推理是轻巧且快速的。您的更改会立即反映在IDE内部,而无需运行代码生成步骤。如果可能的话,它应该是首选方法。但是,当您达到限制时,不要回避倒退到代码生成。对于Zenstack而言,这次后备特别自然,因为TRPC路由器已经来自代码生成。产生更多ð。

没有什么伤害。


希望您喜欢阅读并发现方法很有趣。我们构建了Zenstack工具包,认为强大的模式可以带来许多好处,以简化全堆栈应用程序的构建。如果您喜欢这个主意,请查看我们的GitHub页面以获取更多详细信息!

https://github.com/zenstackhq/zenstack

Star me on github