<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://trpc.io/zh-Hans/blog</id>
    <title>tRPC Blog</title>
    <updated>2025-03-21T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://trpc.io/zh-Hans/blog"/>
    <subtitle>tRPC Blog</subtitle>
    <icon>https://trpc.io/zh-Hans/img/favicon.ico</icon>
    <entry>
        <title type="html"><![CDATA[宣布 tRPC v11]]></title>
        <id>https://trpc.io/zh-Hans/blog/announcing-trpc-v11</id>
        <link href="https://trpc.io/zh-Hans/blog/announcing-trpc-v11"/>
        <updated>2025-03-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[本页面由 PageTurner AI 翻译（测试版）。未经项目官方认可。]]></summary>
        <content type="html"><![CDATA[<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>非官方测试版翻译</div><div class="admonitionContent_e2NW"><p>本页面由 <a href="https://page-turner.com/" target="_blank" rel="noopener noreferrer"><strong>PageTurner</strong></a> AI 翻译（测试版）。未经项目官方认可。
发现错误？ <a href="https://feedback.page-turner.com/?repo_id=683d130a-1828-4b22-91cd-ef2d269ef3f5&amp;file_path=blog%2F2025-03-21-announcing-trpc-11.mdx&amp;locale=zh-Hans" target="_blank" rel="noopener noreferrer">报告问题 →</a></p></div></div>
<!-- -->
<p>尽管通过 <code>@next</code> 标签发布的 tRPC v11 早已具备生产就绪状态，但我们一直沉迷于添加新功能而没有严格遵守语义化版本规范。今天，我们激动地宣布，终于要正式发布 tRPC v11 了！</p>
<!-- -->
<p>自 2022 年 11 月我们发布上一个主要版本以来，tRPC 社区取得了显著增长：</p>
<ul>
<li>
<p>我们在 GitHub 上已收获超过 <a href="https://github.com/trpc/trpc" target="_blank" rel="noopener noreferrer">35,000 颗星</a></p>
</li>
<li>
<p>拥有超过 5,000 名成员的 <a href="https://trpc.io/discord" target="_blank" rel="noopener noreferrer">Discord 社区</a></p>
</li>
<li>
<p><a href="https://www.npmjs.com/package/@trpc/server" target="_blank" rel="noopener noreferrer">每周 npm 下载量超过 70 万次</a></p>
</li>
<li>
<p><a href="https://github.com/trpc/trpc/graphs/contributors" target="_blank" rel="noopener noreferrer">数百名贡献者</a></p>
</li>
<li>
<p><a href="https://trpc.io/awesome" target="_blank" rel="noopener noreferrer">一个_极好的_扩展、示例和内容生态系统</a></p>
</li>
</ul>
<p>随着 tRPC v11 的发布，我们很高兴地告诉大家，v11 版本已经通过我们的 <code>@next</code> 频道稳定演进，并被许多大型 TypeScript 项目用于生产环境</p>
<p>对于新项目，你可以通过<a href="https://trpc.io/awesome#-starting-points-example-projects-etc" target="_blank" rel="noopener noreferrer">示例应用</a>快速上手并学习 tRPC v11。对于仍在使用 tRPC v10 的项目，请<a href="https://trpc.io/docs/v11/migrate-from-v10-to-v11" target="_blank" rel="noopener noreferrer">访问 v11 迁移指南</a>。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="变更概览">变更概览<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-v11#%E5%8F%98%E6%9B%B4%E6%A6%82%E8%A7%88" class="hash-link" aria-label="直接跳转到 变更概览" title="直接跳转到 变更概览">​</a></h2>
<p>v11 与 v10 基本保持向后兼容，但它带来了许多新功能和改进。以下是一些亮点：</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="支持-tanstack-query-v5">支持 TanStack Query v5<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-v11#%E6%94%AF%E6%8C%81-tanstack-query-v5" class="hash-link" aria-label="直接跳转到 支持 TanStack Query v5" title="直接跳转到 支持 TanStack Query v5">​</a></h3>
<p>当 <a href="https://tanstack.com/blog/announcing-tanstack-query-v5" target="_blank" rel="noopener noreferrer">TanStack Query v5</a> 发布时，它要求 tRPC 的 react-query 集成进行一些破坏性变更。这项支持很早就通过 <code>@next</code> 标签提供了，但现在已正式发布。许多项目已经选择升级，并享受到了完整的 React Suspense 支持以及其他诸多改进带来的好处。关于迁移 tRPC React 客户端代码的建议，你可以参考 <a href="https://tanstack.com/query/v5/docs/react/guides/migrating-to-v5" target="_blank" rel="noopener noreferrer">TanStack Query 的迁移指南</a>。</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="全新的-tanstack-react-query-集成">全新的 TanStack React Query 集成<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-v11#%E5%85%A8%E6%96%B0%E7%9A%84-tanstack-react-query-%E9%9B%86%E6%88%90" class="hash-link" aria-label="直接跳转到 全新的 TanStack React Query 集成" title="直接跳转到 全新的 TanStack React Query 集成">​</a></h3>
<p><a href="https://trpc.io/zh-Hans/blog/introducing-tanstack-react-query-client">查看博客文章</a></p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="支持-formdata--非-json-内容类型">支持 FormData / 非 JSON 内容类型<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-v11#%E6%94%AF%E6%8C%81-formdata--%E9%9D%9E-json-%E5%86%85%E5%AE%B9%E7%B1%BB%E5%9E%8B" class="hash-link" aria-label="直接跳转到 支持 FormData / 非 JSON 内容类型" title="直接跳转到 支持 FormData / 非 JSON 内容类型">​</a></h3>
<p>我们最常被要求的功能之一就是能够发送和接收不仅仅是 JSON 数据。借助 tRPC v11，你现在可以发送和接收多种<a href="https://trpc.io/zh-Hans/docs/server/non-json-content-types">非 JSON 内容类型</a>、<code>FormData</code> 以及二进制类型（如 <code>Blob</code>、<code>File</code> 和 <code>Uint8Array</code>）。你可以<a href="https://github.com/trpc/trpc/tree/next/examples/minimal-content-types" target="_blank" rel="noopener noreferrer">在此处</a>找到使用这些内容类型的示例。</p>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
import router">router</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./trpc'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const octetInputParser: UtilityParser<OctetInput, ReadableStream<any>>
import octetInputParser">octetInputParser</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server/http'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'zod'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>>">appRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) router<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>(_: {
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}): BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>>
import router">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) formData: MutationProcedure<{
    input: FormData;
    output: void;
    meta: object;
}>">formData</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodCustom<FormData, FormData>>(schema: z.ZodCustom<FormData, FormData>): ProcedureBuilder<object, object, object, FormData, FormData, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(alias) instanceof<{
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}>(cls: {
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}, params?: {
    when?: ((payload: z.core.ParsePayload) => boolean) | undefined | undefined;
    error?: string | z.core.$ZodErrorMap<z.core.$ZodIssueCustom> | undefined;
    message?: string | undefined | undefined;
}): z.ZodCustom<FormData, FormData>
export instanceof">instanceof</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="var FormData: {
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}">FormData</data-lsp>))</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, FormData, FormData, UnsetMarker, UnsetMarker, false>.mutation<void>(resolver: ProcedureResolver<object, object, object, FormData, UnsetMarker, void>): MutationProcedure<{
    input: FormData;
    output: void;
    meta: object;
}>">mutation</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> ({ <data-lsp lsp="(parameter) input: FormData" style="border-bottom: solid 2px lightgrey;">input</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="meta-line"><span class="popover-prefix">                        </span><span class="popover"><div class="arrow"></div>(parameter) input: FormData</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) file: MutationProcedure<{
    input: OctetInput;
    output: void;
    meta: object;
}>">file</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<UtilityParser<OctetInput, ReadableStream<any>>>(schema: UtilityParser<OctetInput, ReadableStream<any>>): ProcedureBuilder<object, object, object, OctetInput, ReadableStream<any>, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(alias) const octetInputParser: UtilityParser<OctetInput, ReadableStream<any>>
import octetInputParser">octetInputParser</data-lsp>)</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, OctetInput, ReadableStream<any>, UnsetMarker, UnsetMarker, false>.mutation<void>(resolver: ProcedureResolver<object, object, object, ReadableStream<any>, UnsetMarker, void>): MutationProcedure<{
    input: OctetInput;
    output: void;
    meta: object;
}>">mutation</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> ({ <data-lsp lsp="(parameter) input: ReadableStream<any>" style="border-bottom: solid 2px lightgrey;">input</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="meta-line"><span class="popover-prefix">                        </span><span class="popover"><div class="arrow"></div>(parameter) input: ReadableStream&lt;any&gt;</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp>, <data-lsp lsp="(alias) const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
import router">router</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const octetInputParser: UtilityParser<OctetInput, ReadableStream<any>>
import octetInputParser">octetInputParser</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server/http'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'zod'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>>">appRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) router<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>(_: {
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}): BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>>
import router">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) formData: MutationProcedure<{
    input: FormData;
    output: void;
    meta: object;
}>">formData</data-lsp>: <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">    .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodCustom<FormData, FormData>>(schema: z.ZodCustom<FormData, FormData>): ProcedureBuilder<object, object, object, FormData, FormData, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(alias) instanceof<{
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}>(cls: {
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}, params?: {
    when?: ((payload: z.core.ParsePayload) => boolean) | undefined | undefined;
    error?: string | z.core.$ZodErrorMap<z.core.$ZodIssueCustom> | undefined;
    message?: string | undefined | undefined;
}): z.ZodCustom<FormData, FormData>
export instanceof">instanceof</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="var FormData: {
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}">FormData</data-lsp>))</span></div><div class="line"><span style="color: #C9D1D9">    .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, FormData, FormData, UnsetMarker, UnsetMarker, false>.mutation<void>(resolver: ProcedureResolver<object, object, object, FormData, UnsetMarker, void>): MutationProcedure<{
    input: FormData;
    output: void;
    meta: object;
}>">mutation</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> ({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: FormData" style="border-bottom: solid 2px lightgrey;">input</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="meta-line"><span class="popover-prefix">                        </span><span class="popover"><div class="arrow"></div>(parameter) input: FormData</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) file: MutationProcedure<{
    input: OctetInput;
    output: void;
    meta: object;
}>">file</data-lsp>: <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">    .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<UtilityParser<OctetInput, ReadableStream<any>>>(schema: UtilityParser<OctetInput, ReadableStream<any>>): ProcedureBuilder<object, object, object, OctetInput, ReadableStream<any>, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="(alias) const octetInputParser: UtilityParser<OctetInput, ReadableStream<any>>
import octetInputParser">octetInputParser</data-lsp>)</span></div><div class="line"><span style="color: #C9D1D9">    .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, OctetInput, ReadableStream<any>, UnsetMarker, UnsetMarker, false>.mutation<void>(resolver: ProcedureResolver<object, object, object, ReadableStream<any>, UnsetMarker, void>): MutationProcedure<{
    input: OctetInput;
    output: void;
    meta: object;
}>">mutation</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> ({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: ReadableStream<any>" style="border-bottom: solid 2px lightgrey;">input</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="meta-line"><span class="popover-prefix">                        </span><span class="popover"><div class="arrow"></div>(parameter) input: ReadableStream&lt;any&gt;</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="react-服务器组件--nextjs-app-router">React 服务器组件 / Next.js App Router<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-v11#react-%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BB%84%E4%BB%B6--nextjs-app-router" class="hash-link" aria-label="直接跳转到 React 服务器组件 / Next.js App Router" title="直接跳转到 React 服务器组件 / Next.js App Router">​</a></h3>
<p>虽然从一开始你就可以通过以下任一方式将 tRPC 与 Next.js App Router 结合使用：</p>
<ul>
<li>
<p>使用服务端为中心的方式，通过 async/await 搭配 <a href="https://trpc.io/zh-Hans/docs/server/server-side-calls"><code>createCaller</code></a> 或 <a href="https://trpc.io/zh-Hans/docs/client/vanilla"><code>createTRPCClient</code></a></p>
</li>
<li>
<p>或者使用以客户端为中心的方式，利用 <a href="https://trpc.io/zh-Hans/docs/client/react">React Query 集成</a> 和客户端钩子</p>
</li>
</ul>
<p>但两者之间的衔接仍存在粗糙之处。当需要维护不同的数据获取模式时，缓存失效模式变得混杂，导致开发体验不尽如人意，未能达到 tRPC 的标准水准。</p>
<p>为此，我们加强了对 React 服务器组件（RSC）的支持，并新增预取助手函数，使您能更便捷地结合纯服务端运行的 RSC 能力与 React Query 高度动态的客户端缓存。
现在您可以在 RSC 中启动过程执行，在客户端获取待处理的 Promise，并自动向 React Query 缓存注水。这让您能够构建高度动态的应用，同时避免服务端-客户端瀑布流问题。更多细节请参阅<a href="https://trpc.io/zh-Hans/docs/client/react/server-components">服务器组件文档</a>。</p>
<p>除了这些新的预取模式，我们还新增了对服务器函数的实验性支持。您可以在<a href="https://trpc.io/zh-Hans/blog/trpc-actions">博客文章</a>中了解详情。随着服务器函数在生态中逐渐成为成熟模式，我们将持续迭代此功能。</p>
<p>我们还与 <a href="https://x.com/tannerlinsley/status/1844500352655335522" target="_blank" rel="noopener noreferrer">TanStack 团队协作设计了其服务器函数的 API</a>。我们的目标是将 tRPC 强大的中间件系统拆分到独立包中，使其可在整个生态系统中复用。</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="查询和变更的流式响应">查询和变更的流式响应<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-v11#%E6%9F%A5%E8%AF%A2%E5%92%8C%E5%8F%98%E6%9B%B4%E7%9A%84%E6%B5%81%E5%BC%8F%E5%93%8D%E5%BA%94" class="hash-link" aria-label="直接跳转到 查询和变更的流式响应" title="直接跳转到 查询和变更的流式响应">​</a></h3>
<p>我们推出了 <a href="https://trpc.io/zh-Hans/docs/client/links/httpBatchStreamLink">httpBatchStreamLink</a>，支持从查询流式传输响应。这在处理大型数据集或需要实时处理数据并逐步分发到前端时非常有用。该功能并非用于替代订阅，而是为您工具箱增添的又一利器。</p>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #C2C3C5">// @filename: server.ts</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
import router">router</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./trpc'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>">appRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) router<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>(_: {
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}): BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>
import router">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) examples: {
    iterable: QueryProcedure<{
        input: void;
        output: AsyncGenerator<number, never, unknown>;
        meta: object;
    }>;
}">examples</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) iterable: QueryProcedure<{
    input: void;
    output: AsyncGenerator<number, never, unknown>;
    meta: object;
}>">iterable</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<AsyncGenerator<number, never, unknown>>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, AsyncGenerator<number, never, unknown>>): QueryProcedure<{
    input: void;
    output: AsyncGenerator<number, never, unknown>;
    meta: object;
}>">query</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">function*</span><span style="color: #24292EFF"> () {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">let</span><span style="color: #24292EFF"> <data-lsp lsp="let i: number">i</data-lsp> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">0</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">while</span><span style="color: #24292EFF"> (</span><span style="color: #1976D2">true</span><span style="color: #24292EFF">) {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>">Promise</data-lsp></span><span style="color: #24292EFF">((<data-lsp lsp="(parameter) resolve: (value: unknown) => void">resolve</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="function setTimeout(callback: (_: void) => void, delay?: number): NodeJS.Timeout (+2 overloads)
namespace setTimeout">setTimeout</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(parameter) resolve: (value: unknown) => void">resolve</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">500</span><span style="color: #24292EFF">));</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">yield</span><span style="color: #24292EFF"> <data-lsp lsp="let i: number">i</data-lsp></span><span style="color: #D32F2F">++</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">      }</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>">AppRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">typeof</span><span style="color: #24292EFF"> <data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>">appRouter</data-lsp>;</span></div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line"><span style="color: #C2C3C5">// @filename: client.ts</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>
import createTRPCClient">createTRPCClient</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) function httpBatchStreamLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter[&quot;_def&quot;][&quot;_config&quot;][&quot;$types&quot;]>): TRPCLink<TRouter>
import httpBatchStreamLink">httpBatchStreamLink</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/client'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>
import AppRouter">AppRouter</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./server'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const trpc: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>">trpc</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) createTRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>): TRPCClient<...>
import createTRPCClient">createTRPCClient</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>
import AppRouter">AppRouter</data-lsp></span><span style="color: #24292EFF">&gt;({</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) links: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>[]">links</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> [</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1"><data-lsp lsp="(alias) httpBatchStreamLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>(opts: HTTPBatchLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<...>>>
import httpBatchStreamLink">httpBatchStreamLink</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) url: string | URL">url</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'http://localhost:3000'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  ]</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const iterable: AsyncIterable<number, never, unknown>" style="border-bottom: solid 2px lightgrey;">iterable</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const trpc: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>">trpc</data-lsp></span><span style="color: #6F42C1">.</span><span style="color: #1976D2"><data-lsp lsp="(property) examples: DecoratedProcedureRecord<{
    transformer: false;
    errorShape: DefaultErrorShape;
}, DecorateCreateRouterOptions<{
    iterable: QueryProcedure<{
        input: void;
        output: AsyncGenerator<number, never, unknown>;
        meta: object;
    }>;
}>>">examples</data-lsp></span><span style="color: #6F42C1">.</span><span style="color: #1976D2"><data-lsp lsp="(property) iterable: {
    query: Resolver<{
        input: void;
        output: AsyncIterable<number, never, unknown>;
        errorShape: DefaultErrorShape;
        transformer: false;
    }>;
}">iterable</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) query: (input: void, opts?: TRPCProcedureOptions) => Promise<AsyncIterable<number, never, unknown>>">query</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="meta-line"><span class="popover-prefix">         </span><span class="popover"><div class="arrow"></div>const iterable: AsyncIterable&lt;number, never, unknown&gt;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">for</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const value: number">value</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">of</span><span style="color: #24292EFF"> <data-lsp lsp="const iterable: AsyncIterable<number, never, unknown>">iterable</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #1976D2"><data-lsp lsp="namespace console
var console: Console">console</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)">log</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #22863A">'Iterable:'</span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="const value: number" style="border-bottom: solid 2px lightgrey;">value</data-lsp>);</span></div><div class="meta-line"><span class="popover-prefix">                            </span><span class="popover"><div class="arrow"></div>const value: number</span></div><div class="line"><span style="color: #24292EFF">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #8B949E">// @filename: server.ts</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp>, <data-lsp lsp="(alias) const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
import router">router</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>">appRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) router<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>(_: {
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}): BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>
import router">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) examples: {
    iterable: QueryProcedure<{
        input: void;
        output: AsyncGenerator<number, never, unknown>;
        meta: object;
    }>;
}">examples</data-lsp>: {</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) iterable: QueryProcedure<{
    input: void;
    output: AsyncGenerator<number, never, unknown>;
    meta: object;
}>">iterable</data-lsp>: <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<AsyncGenerator<number, never, unknown>>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, AsyncGenerator<number, never, unknown>>): QueryProcedure<{
    input: void;
    output: AsyncGenerator<number, never, unknown>;
    meta: object;
}>">query</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">function*</span><span style="color: #C9D1D9"> () {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">let</span><span style="color: #C9D1D9"> <data-lsp lsp="let i: number">i</data-lsp> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">0</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">while</span><span style="color: #C9D1D9"> (</span><span style="color: #79C0FF">true</span><span style="color: #C9D1D9">) {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>">Promise</data-lsp></span><span style="color: #C9D1D9">((</span><span style="color: #FFA657"><data-lsp lsp="(parameter) resolve: (value: unknown) => void">resolve</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="function setTimeout(callback: (_: void) => void, delay?: number): NodeJS.Timeout (+2 overloads)
namespace setTimeout">setTimeout</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="(parameter) resolve: (value: unknown) => void">resolve</data-lsp>, </span><span style="color: #79C0FF">500</span><span style="color: #C9D1D9">));</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">yield</span><span style="color: #C9D1D9"> <data-lsp lsp="let i: number">i</data-lsp></span><span style="color: #FF7B72">++</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">      }</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  },</span></div><div class="line"><span style="color: #C9D1D9">});</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>">AppRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">typeof</span><span style="color: #C9D1D9"> <data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>">appRouter</data-lsp>;</span></div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line"><span style="color: #8B949E">// @filename: client.ts</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>
import createTRPCClient">createTRPCClient</data-lsp>, <data-lsp lsp="(alias) function httpBatchStreamLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter[&quot;_def&quot;][&quot;_config&quot;][&quot;$types&quot;]>): TRPCLink<TRouter>
import httpBatchStreamLink">httpBatchStreamLink</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/client'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>
import AppRouter">AppRouter</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./server'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const trpc: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>">trpc</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) createTRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>): TRPCClient<...>
import createTRPCClient">createTRPCClient</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(alias) type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>
import AppRouter">AppRouter</data-lsp></span><span style="color: #C9D1D9">&gt;({</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) links: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>[]">links</data-lsp>: [</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) httpBatchStreamLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>(opts: HTTPBatchLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<...>>>
import httpBatchStreamLink">httpBatchStreamLink</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) url: string | URL">url</data-lsp>: </span><span style="color: #A5D6FF">'http://localhost:3000'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  ],</span></div><div class="line"><span style="color: #C9D1D9">});</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const iterable: AsyncIterable<number, never, unknown>" style="border-bottom: solid 2px lightgrey;">iterable</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> <data-lsp lsp="const trpc: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>">trpc</data-lsp>.<data-lsp lsp="(property) examples: DecoratedProcedureRecord<{
    transformer: false;
    errorShape: DefaultErrorShape;
}, DecorateCreateRouterOptions<{
    iterable: QueryProcedure<{
        input: void;
        output: AsyncGenerator<number, never, unknown>;
        meta: object;
    }>;
}>>">examples</data-lsp>.<data-lsp lsp="(property) iterable: {
    query: Resolver<{
        input: void;
        output: AsyncIterable<number, never, unknown>;
        errorShape: DefaultErrorShape;
        transformer: false;
    }>;
}">iterable</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) query: (input: void, opts?: TRPCProcedureOptions) => Promise<AsyncIterable<number, never, unknown>>">query</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="meta-line"><span class="popover-prefix">         </span><span class="popover"><div class="arrow"></div>const iterable: AsyncIterable&lt;number, never, unknown&gt;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">for</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const value: number">value</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">of</span><span style="color: #C9D1D9"> <data-lsp lsp="const iterable: AsyncIterable<number, never, unknown>">iterable</data-lsp>) {</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="namespace console
var console: Console">console</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)">log</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'Iterable:'</span><span style="color: #C9D1D9">, <data-lsp lsp="const value: number" style="border-bottom: solid 2px lightgrey;">value</data-lsp>);</span></div><div class="meta-line"><span class="popover-prefix">                            </span><span class="popover"><div class="arrow"></div>const value: number</span></div><div class="line"><span style="color: #C9D1D9">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="路由定义简写语法">路由定义简写语法<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-v11#%E8%B7%AF%E7%94%B1%E5%AE%9A%E4%B9%89%E7%AE%80%E5%86%99%E8%AF%AD%E6%B3%95" class="hash-link" aria-label="直接跳转到 路由定义简写语法" title="直接跳转到 路由定义简写语法">​</a></h3>
<p>我们引入了新的简写语法来配置路由器，以简化定义路由路径的流程。<a href="https://trpc.io/zh-Hans/docs/server/routers#inline-sub-router">文档</a></p>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}>>">appRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const router: RouterBuilder
<{
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}>(_: {
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// Shorthand plain object for creating a sub-router</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) nested1: {
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}">nested1</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) proc: QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">proc</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<string>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, string>): QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">query</data-lsp></span><span style="color: #24292EFF">(() </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'...'</span><span style="color: #24292EFF">)</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// Equivalent of:</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) nested2: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>>">nested2</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const router: RouterBuilder
<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>(_: {
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}) => BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>>">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) proc: QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">proc</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<string>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, string>): QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">query</data-lsp></span><span style="color: #24292EFF">(() </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'...'</span><span style="color: #24292EFF">)</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}>>">appRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="const router: RouterBuilder
<{
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}>(_: {
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// Shorthand plain object for creating a sub-router</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) nested1: {
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}">nested1</data-lsp>: {</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) proc: QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">proc</data-lsp>: <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<string>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, string>): QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">query</data-lsp></span><span style="color: #C9D1D9">(() </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'...'</span><span style="color: #C9D1D9">),</span></div><div class="line"><span style="color: #C9D1D9">  },</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// Equivalent of:</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) nested2: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>>">nested2</data-lsp>: </span><span style="color: #D2A8FF"><data-lsp lsp="const router: RouterBuilder
<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>(_: {
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}) => BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>>">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) proc: QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">proc</data-lsp>: <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<string>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, string>): QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">query</data-lsp></span><span style="color: #C9D1D9">(() </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'...'</span><span style="color: #C9D1D9">),</span></div><div class="line"><span style="color: #C9D1D9">  }),</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="订阅功能服务器发送事件及其他改进">订阅功能：服务器发送事件及其他改进<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-v11#%E8%AE%A2%E9%98%85%E5%8A%9F%E8%83%BD%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%8F%91%E9%80%81%E4%BA%8B%E4%BB%B6%E5%8F%8A%E5%85%B6%E4%BB%96%E6%94%B9%E8%BF%9B" class="hash-link" aria-label="直接跳转到 订阅功能：服务器发送事件及其他改进" title="直接跳转到 订阅功能：服务器发送事件及其他改进">​</a></h3>
<ul>
<li>
<p>tRPC v11 引入了使用<a href="https://trpc.io/zh-Hans/docs/server/subscriptions#websockets-or-server-sent-events">服务器发送事件（SSE）</a>处理订阅的新方式。这是在不依赖复杂 WebSocket 的情况下实现应用实时更新的理想方案。我们建议优先采用此方案。</p>
</li>
<li>
<p>我们新增了对 JavaScript <a href="https://trpc.io/zh-Hans/docs/server/subscriptions#cleanup-of-side-effects">生成器在订阅中的应用支持</a>。这让您能以更符合 JavaScript 原生特性的方式编写产出多值订阅处理器，并在完成后自动清理资源。</p>
</li>
<li>
<p>订阅功能现支持<a href="https://trpc.io/zh-Hans/docs/server/subscriptions#output-validation">输出验证</a>，增强了订阅处理器的类型安全性。</p>
</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="告别-v9-的-interop-模式">告别 v9 的 <code>.interop()</code> 模式<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-v11#%E5%91%8A%E5%88%AB-v9-%E7%9A%84-interop-%E6%A8%A1%E5%BC%8F" class="hash-link" aria-label="直接跳转到 告别-v9-的-interop-模式" title="直接跳转到 告别-v9-的-interop-模式">​</a></h3>
<p>在 tRPC v10 中，我们引入了 <code>.interop()</code> 模式为 v9 用户提供平滑迁移路径。tRPC v11 已移除 <code>.interop()</code> 模式。若您仍在使用 <code>.interop()</code> 模式，请参考 <a href="https://trpc.io/docs/v10/migrate-from-v9-to-v10" target="_blank" rel="noopener noreferrer">v10 迁移指南</a>完成向现行 API 的过渡。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="迁移至-v11">迁移至 v11<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-v11#%E8%BF%81%E7%A7%BB%E8%87%B3-v11" class="hash-link" aria-label="直接跳转到 迁移至 v11" title="直接跳转到 迁移至 v11">​</a></h2>
<p>若您当前正在使用 tRPC v10，请遵循<a href="https://trpc.io/zh-Hans/docs/migrate-from-v10-to-v11">迁移指南</a>升级至 v11。该指南涵盖了 v11 的所有破坏性变更和新功能。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="致谢">致谢！<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-v11#%E8%87%B4%E8%B0%A2" class="hash-link" aria-label="直接跳转到 致谢！" title="直接跳转到 致谢！">​</a></h2>
<p>tRPC 核心团队全体成员衷心感谢您对 tRPC 的使用与支持。</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="slug-typescript-performance-lessonstitle-v10重构过程中的typescript性能优化经验authors-sachinraja">slug: typescript-performance-lessons
title: v10重构过程中的TypeScript性能优化经验
authors: [sachinraja]<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-v11#slug-typescript-performance-lessonstitle-v10%E9%87%8D%E6%9E%84%E8%BF%87%E7%A8%8B%E4%B8%AD%E7%9A%84typescript%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E7%BB%8F%E9%AA%8Cauthors-sachinraja" class="hash-link" aria-label="直接跳转到 slug: typescript-performance-lessons
title: v10重构过程中的TypeScript性能优化经验
authors: [sachinraja]" title="直接跳转到 slug: typescript-performance-lessons
title: v10重构过程中的TypeScript性能优化经验
authors: [sachinraja]">​</a></h2>
<ul>
<li>
<p>在 Twitter 关注 <a href="https://twitter.com/trpcio" target="_blank" rel="noopener noreferrer">@trpcio</a></p>
</li>
<li>
<p>加入我们的 <a href="https://trpc.io/discord" target="_blank" rel="noopener noreferrer">Discord 社区</a></p>
</li>
<li>
<p><a href="https://trpc.io/#try-it-out" target="_blank" rel="noopener noreferrer">在浏览器中体验 tRPC</a></p>
</li>
</ul>
<a id="sponsor-button" href="https://trpc.io/sponsor" class="group flex h-12 w-max items-center gap-4 rounded-lg border-2 bg-zinc-200 px-4 py-2 transition hover:bg-zinc-100 dark:border-zinc-900 dark:bg-zinc-800 hover:dark:border-zinc-700 hover:dark:bg-zinc-900"><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="aspect-square h-6 fill-pink-500 transition-transform duration-200 ease-in group-hover:scale-110"><path d="M17.625 1.499c-2.32 0-4.354 1.203-5.625 3.03-1.271-1.827-3.305-3.03-5.625-3.03C3.129 1.499 0 4.253 0 8.249c0 4.275 3.068 7.847 5.828 10.227a33.14 33.14 0 0 0 5.616 3.876l.028.017.008.003-.001.003c.163.085.342.126.521.125.179.001.358-.041.521-.125l-.001-.003.008-.003.028-.017a33.14 33.14 0 0 0 5.616-3.876C20.932 16.096 24 12.524 24 8.249c0-3.996-3.129-6.75-6.375-6.75zm-.919 15.275a30.766 30.766 0 0 1-4.703 3.316l-.004-.002-.004.002a30.955 30.955 0 0 1-4.703-3.316c-2.677-2.307-5.047-5.298-5.047-8.523 0-2.754 2.121-4.5 4.125-4.5 2.06 0 3.914 1.479 4.544 3.684.143.495.596.797 1.086.796.49.001.943-.302 1.085-.796.63-2.205 2.484-3.684 4.544-3.684 2.004 0 4.125 1.746 4.125 4.5 0 3.225-2.37 6.216-5.048 8.523z"></path></svg><span class="font-semibold text-zinc-900 no-underline dark:text-zinc-300">成为赞助商！</span></a>]]></content>
        <author>
            <name>tRPC</name>
            <uri>https://trpc.io/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[全新 TanStack React Query 集成方案正式发布]]></title>
        <id>https://trpc.io/zh-Hans/blog/introducing-tanstack-react-query-client</id>
        <link href="https://trpc.io/zh-Hans/blog/introducing-tanstack-react-query-client"/>
        <updated>2025-02-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[本页面由 PageTurner AI 翻译（测试版）。未经项目官方认可。]]></summary>
        <content type="html"><![CDATA[<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>非官方测试版翻译</div><div class="admonitionContent_e2NW"><p>本页面由 <a href="https://page-turner.com/" target="_blank" rel="noopener noreferrer"><strong>PageTurner</strong></a> AI 翻译（测试版）。未经项目官方认可。
发现错误？ <a href="https://feedback.page-turner.com/?repo_id=683d130a-1828-4b22-91cd-ef2d269ef3f5&amp;file_path=blog%2F2025-02-17-new-tanstack-react-query-integration.mdx&amp;locale=zh-Hans" target="_blank" rel="noopener noreferrer">报告问题 →</a></p></div></div>
<p>我们激动地宣布，全新的 TanStack React Query 集成方案已在 tRPC 的 <code>next</code> 版本中推出。相较于传统的 <a href="https://trpc.io/zh-Hans/docs/client/react">React Query 集成方案</a>，新版方案更加简洁且更贴近 TanStack Query 原生范式。它选择直接采用 TanStack React Query 原生的 <a href="https://tanstack.com/query/latest/docs/framework/react/guides/query-options" target="_blank" rel="noopener noreferrer">QueryOptions</a> 和 <a href="https://tanstack.com/query/latest/docs/framework/react/guides/mutations" target="_blank" rel="noopener noreferrer">MutationOptions</a> 接口，而非通过封装 <code>useQuery</code> 和 <code>useMutation</code> 实现自定义客户端。</p>
<!-- -->
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="greeting.tsx"><div class="code-title">greeting.tsx</div><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function useQuery<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = readonly unknown[]>(options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): DefinedUseQueryResult<NoInfer<TData>, TError> (+2 overloads)
import useQuery">useQuery</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@tanstack/react-query'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>
import useTRPC">useTRPC</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./trpc'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">function</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="function Greeting(): null">Greeting</data-lsp></span><span style="color: #24292EFF">() {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>">trpc</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) useTRPC(): TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>
import useTRPC">useTRPC</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const greetingQuery: UseQueryResult<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>>">greetingQuery</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) useQuery<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, string, TRPCQueryKeyWithoutPrefix>(options: UndefinedInitialDataOptions<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, string, TRPCQueryKeyWithoutPrefix>, queryClient?: QueryClient): UseQueryResult<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>> (+2 overloads)
import useQuery">useQuery</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>">trpc</data-lsp></span><span style="color: #6F42C1">.</span><span style="color: #1976D2"><data-lsp lsp="(property) greeting: DecorateQueryProcedure<{
    input: {
        name: string;
    };
    output: string;
    transformer: false;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}> &amp; Record<string, never>">greeting</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) DecorateQueryProcedure<{ input: { name: string; }; output: string; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.queryOptions: TRPCQueryOptions
<string, string>(input: {
    name: string;
}, opts?: UnusedSkipTokenTRPCQueryOptionsIn<string, string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, {
    keyPrefix: false;
}> | undefined) => UnusedSkipTokenTRPCQueryOptionsOut<string, string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, {
    keyPrefix: false;
}> (+2 overloads)">queryOptions</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) name: string">name</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'Jerry'</span><span style="color: #24292EFF"> }));</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// greetingQuery.data === 'Hello Jerry'</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">null</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="greeting.tsx"><div class="code-title">greeting.tsx</div><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function useQuery<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = readonly unknown[]>(options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): DefinedUseQueryResult<NoInfer<TData>, TError> (+2 overloads)
import useQuery">useQuery</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@tanstack/react-query'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>
import useTRPC">useTRPC</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">function</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="function Greeting(): null">Greeting</data-lsp></span><span style="color: #C9D1D9">() {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>">trpc</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) useTRPC(): TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>
import useTRPC">useTRPC</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const greetingQuery: UseQueryResult<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>>">greetingQuery</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) useQuery<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, string, TRPCQueryKeyWithoutPrefix>(options: UndefinedInitialDataOptions<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, string, TRPCQueryKeyWithoutPrefix>, queryClient?: QueryClient): UseQueryResult<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>> (+2 overloads)
import useQuery">useQuery</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>">trpc</data-lsp>.<data-lsp lsp="(property) greeting: DecorateQueryProcedure<{
    input: {
        name: string;
    };
    output: string;
    transformer: false;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}> &amp; Record<string, never>">greeting</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) DecorateQueryProcedure<{ input: { name: string; }; output: string; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.queryOptions: TRPCQueryOptions
<string, string>(input: {
    name: string;
}, opts?: UnusedSkipTokenTRPCQueryOptionsIn<string, string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, {
    keyPrefix: false;
}> | undefined) => UnusedSkipTokenTRPCQueryOptionsOut<string, string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, {
    keyPrefix: false;
}> (+2 overloads)">queryOptions</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) name: string">name</data-lsp>: </span><span style="color: #A5D6FF">'Jerry'</span><span style="color: #C9D1D9"> }));</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// greetingQuery.data === 'Hello Jerry'</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">null</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>通过新版客户端，我们移除了常令新用户困惑的抽象层，转而提供更直接的 TanStack React Query 工作流。对于熟悉 TanStack 官方<a href="https://tanstack.com/query/latest" target="_blank" rel="noopener noreferrer">文档</a>的开发者而言，这将带来即时的熟悉感。这也意味着我们无需过多 tRPC 文档来解释其原理——当然，我们仍准备了<a href="https://trpc.io/zh-Hans/docs/client/tanstack-react-query/setup">入门指南</a>供您参考。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="为何做出这一改变">为何做出这一改变？<a href="https://trpc.io/zh-Hans/blog/introducing-tanstack-react-query-client#%E4%B8%BA%E4%BD%95%E5%81%9A%E5%87%BA%E8%BF%99%E4%B8%80%E6%94%B9%E5%8F%98" class="hash-link" aria-label="直接跳转到 为何做出这一改变？" title="直接跳转到 为何做出这一改变？">​</a></h2>
<p>您可通过<a href="https://github.com/trpc/trpc/discussions/6240" target="_blank" rel="noopener noreferrer">此链接</a>查阅详细的原始 RFC 提案，以下是我们做出改变的核心原因：</p>
<ul>
<li>
<p><strong>简洁性</strong>：新版客户端更简洁且更契合 TanStack Query 原生范式，为 QueryKeys、QueryOptions 和 MutationOptions 等常用接口提供工厂方法。这降低了学习曲线，您可直接参考<a href="https://tanstack.com/query/latest" target="_blank" rel="noopener noreferrer">官方 TanStack Query 文档</a></p>
</li>
<li>
<p><strong>熟悉度</strong>：新版客户端让已在其他场景使用 TanStack Query 的开发者倍感熟悉，避免因使用 tRPC 而被迫切换语法范式</p>
</li>
<li>
<p><strong>React 兼容性</strong>：传统集成方案实际违反了 Hook 规则，<a href="https://github.com/trpc/trpc/issues/2330" target="_blank" rel="noopener noreferrer">无法通过正确性检查</a>，且会鼓励传递 Hook 作为 props 等破坏 React Compiler 的模式。新版客户端在这方面更符合 React 最佳实践</p>
</li>
<li>
<p><strong>可维护性</strong>：版本同步始终是我们面临的挑战，尤其是当 TanStack Query 的 QueryClient 新增功能时。通过原生接口的精简接入层，我们既能更轻松地支持 React Query，也遵循了 <a href="https://bsky.app/profile/tkdodo.eu/post/3lgizrcvjmc24" target="_blank" rel="noopener noreferrer">TanStack 维护者推崇的最佳实践</a></p>
</li>
<li>
<p><strong>社区反馈</strong>：传统客户端常为新用户制造理解障碍，但针对新版客户端的 <a href="https://github.com/trpc/trpc/discussions/6240" target="_blank" rel="noopener noreferrer">RFC 提案</a>获得了压倒性积极反馈，多数留言用户都期待使用此方案。当然目前并非所有用户都接受新版方案，因此我们将继续维护传统客户端</p>
</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="现有的-trpc-react-query-集成将如何发展">现有的 tRPC React Query 集成将如何发展？<a href="https://trpc.io/zh-Hans/blog/introducing-tanstack-react-query-client#%E7%8E%B0%E6%9C%89%E7%9A%84-trpc-react-query-%E9%9B%86%E6%88%90%E5%B0%86%E5%A6%82%E4%BD%95%E5%8F%91%E5%B1%95" class="hash-link" aria-label="直接跳转到 现有的 tRPC React Query 集成将如何发展？" title="直接跳转到 现有的 tRPC React Query 集成将如何发展？">​</a></h2>
<p>它不会很快消失！我们承诺将长期维护传统方案，但不再为其增加重大新功能，并将其视为稳定版本。</p>
<p>我们仍建议新项目采用新版 TanStack React Query 集成方案，现有项目可考虑逐步迁移。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="如何迁移">如何迁移？<a href="https://trpc.io/zh-Hans/blog/introducing-tanstack-react-query-client#%E5%A6%82%E4%BD%95%E8%BF%81%E7%A7%BB" class="hash-link" aria-label="直接跳转到 如何迁移？" title="直接跳转到 如何迁移？">​</a></h2>
<p>虽然传统客户端将长期维护，但我们建议新项目直接采用新版方案，活跃项目可考虑渐进式迁移。</p>
<p>两个客户端相互兼容且可共存于同一应用，因此您可按自身节奏迁移。我们正在开发自动化迁移工具，并<strong>热切期待</strong>社区贡献！特别感谢 <a href="https://bsky.app/profile/reaper.is" target="_blank" rel="noopener noreferrer">@reaper</a> 目前为此工具做出的贡献！</p>
<p>👉 <a href="https://trpc.io/zh-Hans/docs/client/tanstack-react-query/migrating">阅读迁移文档</a></p>]]></content>
        <author>
            <name>Julius Marminge</name>
            <uri>https://www.jumr.dev/</uri>
        </author>
        <author>
            <name>Nick Lucas</name>
            <uri>https://bsky.app/profile/nicklucas.dev</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[在 tRPC 中使用服务器动作]]></title>
        <id>https://trpc.io/zh-Hans/blog/trpc-actions</id>
        <link href="https://trpc.io/zh-Hans/blog/trpc-actions"/>
        <updated>2024-05-23T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[本页面由 PageTurner AI 翻译（测试版）。未经项目官方认可。]]></summary>
        <content type="html"><![CDATA[<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>非官方测试版翻译</div><div class="admonitionContent_e2NW"><p>本页面由 <a href="https://page-turner.com/" target="_blank" rel="noopener noreferrer"><strong>PageTurner</strong></a> AI 翻译（测试版）。未经项目官方认可。
发现错误？ <a href="https://feedback.page-turner.com/?repo_id=683d130a-1828-4b22-91cd-ef2d269ef3f5&amp;file_path=blog%2F2024-05-23-trpc-actions.mdx&amp;locale=zh-Hans" target="_blank" rel="noopener noreferrer">报告问题 →</a></p></div></div>
<!-- -->
<p>tRPC v10 引入的过程创建器模式（builder-pattern）获得了社区的高度认可，许多库都采用了类似模式。甚至出现了<code>tRPC like XYZ</code>这样的术语，证明该模式日益流行。事实上，最近我看到<a href="https://x.com/localhost_5173/status/1793259910723215835" target="_blank" rel="noopener noreferrer">有人询问是否能用类似 tRPC 的 API 编写 CLI 应用</a>。顺带一提，你甚至可以直接<a href="https://github.com/mmkal/trpc-cli" target="_blank" rel="noopener noreferrer">使用 tRPC 实现这个需求</a>。但今天我们要讨论的重点不是这个，而是如何在 Next.js 的服务器动作中使用 tRPC。</p>
<!-- -->
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="什么是服务器动作">什么是服务器动作？<a href="https://trpc.io/zh-Hans/blog/trpc-actions#%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%8A%A8%E4%BD%9C" class="hash-link" aria-label="直接跳转到 什么是服务器动作？" title="直接跳转到 什么是服务器动作？">​</a></h2>
<p>如果你最近没有关注 <a href="https://react.dev/reference/rsc/server-actions" target="_blank" rel="noopener noreferrer">React</a> 和 <a href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations" target="_blank" rel="noopener noreferrer">Next.js</a> 的最新特性，服务器动作允许你编写在服务端执行的常规函数，然后在客户端导入并像普通函数一样调用。你可能觉得这听起来和 tRPC 很相似——确实如此。根据 <a href="https://x.com/dan_abramov2" target="_blank" rel="noopener noreferrer">Dan Abramov</a> 的说法，服务器动作是"打包器特性版的 tRPC"：</p>
<div class="mb-4 flex w-full justify-center"><div class="react-tweet-theme root_gzUe root_OzMz"><article class="article_zVA9"><span class="skeleton_XJfA" style="height:3rem;margin-bottom:0.75rem"></span><span class="skeleton_XJfA" style="height:6rem;margin:0.5rem 0"></span><div style="border-top:var(--tweet-border);margin:0.5rem 0"></div><span class="skeleton_XJfA" style="height:2rem"></span><span class="skeleton_XJfA" style="height:2rem;border-radius:9999px;margin-top:0.5rem"></span></article></div></div>
<p>这个描述非常准确，服务器动作确实类似于 tRPC，毕竟它们本质上都是 <a href="https://en.wikipedia.org/wiki/Remote_procedure_call" target="_blank" rel="noopener noreferrer">RPC</a>。两者都允许你在后端编写函数，并在前端通过抽象化的网络层以完全类型安全的方式调用。</p>
<p>那么 tRPC 的定位是什么？为什么需要同时使用 tRPC 和服务器动作？服务器动作是基础原语，而所有原语都相对简陋，在构建 API 时缺乏关键能力。任何通过网络暴露的 API 端点都需要验证和授权机制来防止恶意使用。如前所述，tRPC 的 API 设计广受好评，那么能否用 tRPC 定义服务器动作，并利用其内置的强大功能呢？包括输入验证、通过中间件实现的认证鉴权、输出验证、数据转换器等。我认为完全可以，下面让我们深入探讨。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="使用-trpc-定义服务器动作">使用 tRPC 定义服务器动作<a href="https://trpc.io/zh-Hans/blog/trpc-actions#%E4%BD%BF%E7%94%A8-trpc-%E5%AE%9A%E4%B9%89%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%8A%A8%E4%BD%9C" class="hash-link" aria-label="直接跳转到 使用 tRPC 定义服务器动作" title="直接跳转到 使用 tRPC 定义服务器动作">​</a></h2>
<div class="theme-admonition theme-admonition-note admonition_v5Ag alert alert--secondary"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>备注</div><div class="admonitionContent_e2NW"><p><strong>先决条件：</strong> 使用服务器动作需要 Next.js App Router。此外，我们将使用的 tRPC 功能仅适用于 v11 版本，请确保使用 tRPC 的 beta 发布渠道：</p><div class="tabs-container tabList_mG0D"><ul role="tablist" aria-orientation="horizontal" class="tabs"><li role="tab" tabindex="0" aria-selected="true" class="tabs__item tabItem_F3SZ tabs__item--active">npm</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_F3SZ">yarn</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_F3SZ">pnpm</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_F3SZ">bun</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_F3SZ">deno</li></ul><div class="margin-top--md"><div role="tabpanel" class="tabItem_f_Dr"><pre language="bash" title="shell" class="InstallationSnippet__CodeBlock">npm install @trpc/server<button type="button" aria-label="复制代码到剪贴板" class="copy-button">复制</button></pre></div><div role="tabpanel" class="tabItem_f_Dr" hidden=""><pre language="bash" title="shell" class="InstallationSnippet__CodeBlock">yarn add @trpc/server<button type="button" aria-label="复制代码到剪贴板" class="copy-button">复制</button></pre></div><div role="tabpanel" class="tabItem_f_Dr" hidden=""><pre language="bash" title="shell" class="InstallationSnippet__CodeBlock">pnpm add @trpc/server<button type="button" aria-label="复制代码到剪贴板" class="copy-button">复制</button></pre></div><div role="tabpanel" class="tabItem_f_Dr" hidden=""><pre language="bash" title="shell" class="InstallationSnippet__CodeBlock">bun add @trpc/server<button type="button" aria-label="复制代码到剪贴板" class="copy-button">复制</button></pre></div><div role="tabpanel" class="tabItem_f_Dr" hidden=""><pre language="bash" title="shell" class="InstallationSnippet__CodeBlock">deno add npm:@trpc/server<button type="button" aria-label="复制代码到剪贴板" class="copy-button">复制</button></pre></div></div></div></div></div>
<p>让我们从初始化 tRPC 和定义基础服务器动作过程开始。我们将使用过程构建器上的 <code>experimental_caller</code> 方法——这个新方法允许自定义过程被函数调用时的执行方式。同时使用适配器 <code>experimental_nextAppDirCaller</code> 使其兼容 Next.js。该适配器会处理客户端使用 <code>useActionState</code> 包裹服务器动作的情况，这种情况会<a href="https://react.dev/reference/react/useActionState#my-action-can-no-longer-read-the-submitted-form-data" target="_blank" rel="noopener noreferrer">改变服务器动作的调用签名</a>。</p>
<p>由于没有常规路由路径（例如 <code>user.byId</code>），我们将使用 <code>span</code> 属性作为<a href="https://trpc.io/zh-Hans/docs/server/metadata">元数据</a>。你可以利用这个属性区分不同过程，例如在日志记录或可观测性场景中。</p>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server/adapters/next-app-dir'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">interface</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">&gt;()</span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #6F42C1">.</span><span style="color: #1976D2"><data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> ({ <data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">);</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>, <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server/adapters/next-app-dir'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">interface</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">&gt;().</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp>.<data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #D2A8FF"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #C9D1D9">: ({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) meta: unknown">meta</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">  }),</span></div><div class="line"><span style="color: #C9D1D9">);</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>接下来我们将添加<a href="https://trpc.io/zh-Hans/docs/server/context">上下文</a>。由于不会通过常规 HTTP 适配器托管路由，我们无法通过适配器的 <code>createContext</code> 方法注入上下文。取而代之的是使用中间件注入上下文。本例中，我们将从会话中获取当前用户并注入上下文。</p>
<div></div>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server/adapters/next-app-dir'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./auth'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">interface</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">&gt;()</span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> ({ <data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  )</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User | null;
}>(fn: MiddlewareBuilder<{}, Meta, {
    user: User | null;
}, UnsetMarker> | MiddlewareFunction<object, Meta, object, {
    user: User | null;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #C2C3C5">// Inject user into context</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const user: User | null">user</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) next: <{
    user: User | null;
}>(opts: {
    ctx?: {
        user: User | null;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User | null;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) ctx?: {
    user: User | null;
} | undefined">ctx</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> { <data-lsp lsp="(property) user: User | null">user</data-lsp> } });</span></div><div class="line"><span style="color: #24292EFF">  });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>, <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server/adapters/next-app-dir'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./auth'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">interface</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">&gt;().</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp>.<data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #D2A8FF"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #C9D1D9">: ({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) meta: unknown">meta</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  )</span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User | null;
}>(fn: MiddlewareBuilder<{}, Meta, {
    user: User | null;
}, UnsetMarker> | MiddlewareFunction<object, Meta, object, {
    user: User | null;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #8B949E">// Inject user into context</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const user: User | null">user</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) next: <{
    user: User | null;
}>(opts: {
    ctx?: {
        user: User | null;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User | null;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) ctx?: {
    user: User | null;
} | undefined">ctx</data-lsp>: { <data-lsp lsp="(property) user: User | null">user</data-lsp> } });</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>最后，我们将创建一个 <code>protectedAction</code> 过程，用于保护所有操作免受未认证用户的访问。如果你已有现成的中间件实现此功能，可以直接复用；这里我将以内联方式定义一个示例。</p>
<div></div>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server/adapters/next-app-dir'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./auth'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">interface</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">&gt;()</span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> ({ <data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  )</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User | null;
}>(fn: MiddlewareBuilder<{}, Meta, {
    user: User | null;
}, UnsetMarker> | MiddlewareFunction<object, Meta, object, {
    user: User | null;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #C2C3C5">// Inject user into context</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const user: User | null" style="border-bottom: solid 2px lightgrey;">user</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="meta-line"><span class="popover-prefix">           </span><span class="popover"><div class="arrow"></div>const user: User | null</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) next: <{
    user: User | null;
}>(opts: {
    ctx?: {
        user: User | null;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User | null;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) ctx?: {
    user: User | null;
} | undefined">ctx</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> { <data-lsp lsp="(property) user: User | null">user</data-lsp> } });</span></div><div class="line"><span style="color: #24292EFF">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">protectedAction</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User | null; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User;
}>(fn: MiddlewareBuilder<{
    user: User | null;
}, Meta, {
    user: User;
}, UnsetMarker> | MiddlewareFunction<object, Meta, {
    user: User | null;
}, {
    user: User;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #24292EFF">((<data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">!</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) user: User | null">user</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">throw</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) code: &quot;UNAUTHORIZED&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;NOT_FOUND&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'UNAUTHORIZED'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    });</span></div><div class="line"><span style="color: #24292EFF">  }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) next: <{
    user: User;
}>(opts: {
    ctx?: {
        user: User;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) ctx?: {
    user: User;
} | undefined">ctx</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">...</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) user: User" style="border-bottom: solid 2px lightgrey;">user</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) user: User" style="border-bottom: solid 2px lightgrey;">user</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #C2C3C5">// &lt;-- ensures type is non-nullable</span></div><div class="meta-line"><span class="popover-prefix">       </span><span class="popover"><div class="arrow"></div>(property) user: User</span></div><div class="line"><span style="color: #24292EFF">    }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  });</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>, <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server/adapters/next-app-dir'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./auth'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">interface</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">&gt;().</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp>.<data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #D2A8FF"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #C9D1D9">: ({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) meta: unknown">meta</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  )</span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User | null;
}>(fn: MiddlewareBuilder<{}, Meta, {
    user: User | null;
}, UnsetMarker> | MiddlewareFunction<object, Meta, object, {
    user: User | null;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #8B949E">// Inject user into context</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const user: User | null" style="border-bottom: solid 2px lightgrey;">user</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="meta-line"><span class="popover-prefix">           </span><span class="popover"><div class="arrow"></div>const user: User | null</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) next: <{
    user: User | null;
}>(opts: {
    ctx?: {
        user: User | null;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User | null;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) ctx?: {
    user: User | null;
} | undefined">ctx</data-lsp>: { <data-lsp lsp="(property) user: User | null">user</data-lsp> } });</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">protectedAction</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User | null; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User;
}>(fn: MiddlewareBuilder<{
    user: User | null;
}, Meta, {
    user: User;
}, UnsetMarker> | MiddlewareFunction<object, Meta, {
    user: User | null;
}, {
    user: User;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #C9D1D9">((</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.<data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp>.<data-lsp lsp="(property) user: User | null">user</data-lsp>) {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">throw</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) code: &quot;UNAUTHORIZED&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;NOT_FOUND&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp>: </span><span style="color: #A5D6FF">'UNAUTHORIZED'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    });</span></div><div class="line"><span style="color: #C9D1D9">  }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) next: <{
    user: User;
}>(opts: {
    ctx?: {
        user: User;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) ctx?: {
    user: User;
} | undefined">ctx</data-lsp>: {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">...</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.<data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) user: User" style="border-bottom: solid 2px lightgrey;">user</data-lsp>: <data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.<data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp>.<data-lsp lsp="(property) user: User" style="border-bottom: solid 2px lightgrey;">user</data-lsp>, </span><span style="color: #8B949E">// &lt;-- ensures type is non-nullable</span></div><div class="meta-line"><span class="popover-prefix">       </span><span class="popover"><div class="arrow"></div>(property) user: User</span></div><div class="line"><span style="color: #C9D1D9">    },</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>现在，让我们编写一个实际的服务器操作。创建一个 <code>_actions.ts</code> 文件，添加 <code>"use server"</code> 指令，然后定义你的操作。</p>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="app/_actions.ts"><div class="code-title">app/_actions.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #22863A">'use server'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'zod'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import protectedAction">protectedAction</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'../server/trpc'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const createPost: (input: {
    title: string;
}) => Promise<void>">createPost</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import protectedAction">protectedAction</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, Meta, {
    user: User;
}, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, true>">input</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) title: z.ZodString">title</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  )</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, true>.mutation<void>(resolver: ProcedureResolver<object, Meta, {
    user: User;
}, {
    title: string;
}, UnsetMarker, void>): (input: {
    title: string;
}) => Promise<void>">mutation</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    title: string;
}>">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #C2C3C5">// Do something with the input</span></div><div class="line"><span style="color: #24292EFF">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C2C3C5">// Since we're using the `experimental_caller`,</span></div><div class="line"><span style="color: #C2C3C5">// our procedure is now just an ordinary function:</span></div><div class="line"><span style="color: #24292EFF"><data-lsp lsp="const createPost: (input: {
    title: string;
}) => Promise<void>" style="border-bottom: solid 2px lightgrey;">createPost</data-lsp>;</span></div><div class="meta-line"><span class="popover-prefix">    </span><span class="popover"><div class="arrow"></div>const createPost: (input: {
    title: string;
}) =&gt; Promise&lt;void&gt;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="app/_actions.ts"><div class="code-title">app/_actions.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #A5D6FF">'use server'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'zod'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import protectedAction">protectedAction</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'../server/trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const createPost: (input: {
    title: string;
}) => Promise<void>">createPost</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(alias) const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import protectedAction">protectedAction</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, Meta, {
    user: User;
}, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, true>">input</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) title: z.ZodString">title</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  )</span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, true>.mutation<void>(resolver: ProcedureResolver<object, Meta, {
    user: User;
}, {
    title: string;
}, UnsetMarker, void>): (input: {
    title: string;
}) => Promise<void>">mutation</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    title: string;
}>">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #8B949E">// Do something with the input</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #8B949E">// Since we're using the `experimental_caller`,</span></div><div class="line"><span style="color: #8B949E">// our procedure is now just an ordinary function:</span></div><div class="line"><span style="color: #C9D1D9"><data-lsp lsp="const createPost: (input: {
    title: string;
}) => Promise<void>" style="border-bottom: solid 2px lightgrey;">createPost</data-lsp>;</span></div><div class="meta-line"><span class="popover-prefix">    </span><span class="popover"><div class="arrow"></div>const createPost: (input: {
    title: string;
}) =&gt; Promise&lt;void&gt;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>看，定义服务器操作竟如此简单：既能防止未认证用户访问，又通过输入验证抵御 SQL 注入等攻击。现在让我们在客户端导入并调用这个函数。</p>
<div><pre class="shiki min-light with-title" style="background-color: #ffffff; color: #24292eff" title="app/post-form.tsx"><div class="code-title">app/post-form.tsx</div><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #22863A">'use client'</span><span style="color: #24292EFF">;</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { createPost } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./_actions'</span><span style="color: #24292EFF">;</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">function</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">PostForm</span><span style="color: #24292EFF">() {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> (</span></div><div class="line"><span style="color: #24292EFF">    &lt;</span><span style="color: #22863A">form</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #C2C3C5">// Use `action` to make form progressively enhanced</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">action</span><span style="color: #D32F2F">=</span><span style="color: #24292EFF">{createPost}</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #C2C3C5">// `Using `onSubmit` allows building rich interactive</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #C2C3C5">// forms once JavaScript has loaded</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">onSubmit</span><span style="color: #D32F2F">=</span><span style="color: #24292EFF">{</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (e) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #1976D2">e</span><span style="color: #6F42C1">.preventDefault</span><span style="color: #24292EFF">();</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">title</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">FormData</span><span style="color: #24292EFF">(</span><span style="color: #1976D2">e</span><span style="color: #24292EFF">.currentTarget)</span><span style="color: #6F42C1">.get</span><span style="color: #24292EFF">(</span><span style="color: #22863A">'title'</span><span style="color: #24292EFF">) </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #C2C3C5">// Maybe show loading toast, etc etc. Endless possibilities</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">createPost</span><span style="color: #24292EFF">({ title });</span></div><div class="line"><span style="color: #24292EFF">      }}</span></div><div class="line"><span style="color: #24292EFF">    &gt;</span></div><div class="line"><span style="color: #24292EFF">      &lt;</span><span style="color: #22863A">input</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">type</span><span style="color: #D32F2F">=</span><span style="color: #22863A">"text"</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">name</span><span style="color: #D32F2F">=</span><span style="color: #22863A">"title"</span><span style="color: #24292EFF"> /&gt;</span></div><div class="line"><span style="color: #24292EFF">      &lt;</span><span style="color: #22863A">button</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">type</span><span style="color: #D32F2F">=</span><span style="color: #22863A">"submit"</span><span style="color: #24292EFF">&gt;Create Post&lt;/</span><span style="color: #22863A">button</span><span style="color: #24292EFF">&gt;</span></div><div class="line"><span style="color: #24292EFF">    &lt;/</span><span style="color: #22863A">form</span><span style="color: #24292EFF">&gt;</span></div><div class="line"><span style="color: #24292EFF">  );</span></div><div class="line"><span style="color: #24292EFF">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title" style="background-color: #0d1117; color: #c9d1d9" title="app/post-form.tsx"><div class="code-title">app/post-form.tsx</div><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #A5D6FF">'use client'</span><span style="color: #C9D1D9">;</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { createPost } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./_actions'</span><span style="color: #C9D1D9">;</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">function</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">PostForm</span><span style="color: #C9D1D9">() {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> (</span></div><div class="line"><span style="color: #C9D1D9">    &lt;</span><span style="color: #7EE787">form</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #8B949E">// Use `action` to make form progressively enhanced</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #79C0FF">action</span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9">{createPost}</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #8B949E">// `Using `onSubmit` allows building rich interactive</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #8B949E">// forms once JavaScript has loaded</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #79C0FF">onSubmit</span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9">{</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657">e</span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        e.</span><span style="color: #D2A8FF">preventDefault</span><span style="color: #C9D1D9">();</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">title</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">FormData</span><span style="color: #C9D1D9">(e.currentTarget).</span><span style="color: #D2A8FF">get</span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'title'</span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #8B949E">// Maybe show loading toast, etc etc. Endless possibilities</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">createPost</span><span style="color: #C9D1D9">({ title });</span></div><div class="line"><span style="color: #C9D1D9">      }}</span></div><div class="line"><span style="color: #C9D1D9">    &gt;</span></div><div class="line"><span style="color: #C9D1D9">      &lt;</span><span style="color: #7EE787">input</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">type</span><span style="color: #FF7B72">=</span><span style="color: #A5D6FF">"text"</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">name</span><span style="color: #FF7B72">=</span><span style="color: #A5D6FF">"title"</span><span style="color: #C9D1D9"> /&gt;</span></div><div class="line"><span style="color: #C9D1D9">      &lt;</span><span style="color: #7EE787">button</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">type</span><span style="color: #FF7B72">=</span><span style="color: #A5D6FF">"submit"</span><span style="color: #C9D1D9">&gt;Create Post&lt;/</span><span style="color: #7EE787">button</span><span style="color: #C9D1D9">&gt;</span></div><div class="line"><span style="color: #C9D1D9">    &lt;/</span><span style="color: #7EE787">form</span><span style="color: #C9D1D9">&gt;</span></div><div class="line"><span style="color: #C9D1D9">  );</span></div><div class="line"><span style="color: #C9D1D9">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="高级扩展">高级扩展<a href="https://trpc.io/zh-Hans/blog/trpc-actions#%E9%AB%98%E7%BA%A7%E6%89%A9%E5%B1%95" class="hash-link" aria-label="直接跳转到 高级扩展" title="直接跳转到 高级扩展">​</a></h2>
<p>利用 tRPC 的构建器模式及其可组合的过程定义能力，我们可以轻松构建更复杂的服务器操作。以下是一些示例：</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="可观测性">可观测性<a href="https://trpc.io/zh-Hans/blog/trpc-actions#%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7" class="hash-link" aria-label="直接跳转到 可观测性" title="直接跳转到 可观测性">​</a></h3>
<p>只需几行代码即可使用 <code>@baselime/node-opentelemtry</code> 的 tRPC 插件实现可观测性：</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">diff</div><div class="code-container"><code><div class="line"><span style="color: #24292EFF">--- server/trpc.ts</span></div><div class="line"><span style="color: #24292EFF">+++ server/trpc.ts</span></div><div class="line"><span style="color: #24292EFF">+ import { tracing } from '@baselime/node-opentelemetry/trpc';</span></div><div class="line"></div><div class="line"><span style="color: #24292EFF">  export const serverActionProcedure = t.procedure</span></div><div class="line"><span style="color: #24292EFF">    .experimental_caller(</span></div><div class="line"><span style="color: #24292EFF">      experimental_nextAppDirCaller({</span></div><div class="line"><span style="color: #24292EFF">        pathExtractor: (meta: Meta) =&gt; meta.span,</span></div><div class="line"><span style="color: #24292EFF">      }),</span></div><div class="line"><span style="color: #24292EFF">    )</span></div><div class="line"><span style="color: #24292EFF">    .use(async (opts) =&gt; {</span></div><div class="line"><span style="color: #24292EFF">      // Inject user into context</span></div><div class="line"><span style="color: #24292EFF">      const user = await currentUser();</span></div><div class="line"><span style="color: #24292EFF">      return opts.next({ ctx: { user } });</span></div><div class="line"><span style="color: #24292EFF">    })</span></div><div class="line"><span style="color: #24292EFF">+  .use(tracing());</span></div><div class="line"></div><div class="line"><span style="color: #24292EFF">--- app/_actions.ts</span></div><div class="line"><span style="color: #24292EFF">+++ app/_actions.ts</span></div><div class="line"><span style="color: #24292EFF">  export const createPost = protectedAction</span></div><div class="line"><span style="color: #24292EFF">+   .meta({ span: 'create-post' })</span></div><div class="line"><span style="color: #24292EFF">    .input(</span></div><div class="line"><span style="color: #24292EFF">      z.object({</span></div><div class="line"><span style="color: #24292EFF">        title: z.string(),</span></div><div class="line"><span style="color: #24292EFF">      }),</span></div><div class="line"><span style="color: #24292EFF">    )</span></div><div class="line"><span style="color: #24292EFF">    .mutation(async (opts) =&gt; {</span></div><div class="line"><span style="color: #24292EFF">      // Do something with the input</span></div><div class="line"><span style="color: #24292EFF">    });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">diff</div><div class="code-container"><code><div class="line"><span style="color: #FFA198">--- server/trpc.ts</span></div><div class="line"><span style="color: #7EE787">+++ server/trpc.ts</span></div><div class="line"><span style="color: #7EE787">+ import { tracing } from '@baselime/node-opentelemetry/trpc';</span></div><div class="line"></div><div class="line"><span style="color: #C9D1D9">  export const serverActionProcedure = t.procedure</span></div><div class="line"><span style="color: #C9D1D9">    .experimental_caller(</span></div><div class="line"><span style="color: #C9D1D9">      experimental_nextAppDirCaller({</span></div><div class="line"><span style="color: #C9D1D9">        pathExtractor: (meta: Meta) =&gt; meta.span,</span></div><div class="line"><span style="color: #C9D1D9">      }),</span></div><div class="line"><span style="color: #C9D1D9">    )</span></div><div class="line"><span style="color: #C9D1D9">    .use(async (opts) =&gt; {</span></div><div class="line"><span style="color: #C9D1D9">      // Inject user into context</span></div><div class="line"><span style="color: #C9D1D9">      const user = await currentUser();</span></div><div class="line"><span style="color: #C9D1D9">      return opts.next({ ctx: { user } });</span></div><div class="line"><span style="color: #C9D1D9">    })</span></div><div class="line"><span style="color: #7EE787">+  .use(tracing());</span></div><div class="line"></div><div class="line"><span style="color: #FFA198">--- app/_actions.ts</span></div><div class="line"><span style="color: #7EE787">+++ app/_actions.ts</span></div><div class="line"><span style="color: #C9D1D9">  export const createPost = protectedAction</span></div><div class="line"><span style="color: #7EE787">+   .meta({ span: 'create-post' })</span></div><div class="line"><span style="color: #C9D1D9">    .input(</span></div><div class="line"><span style="color: #C9D1D9">      z.object({</span></div><div class="line"><span style="color: #C9D1D9">        title: z.string(),</span></div><div class="line"><span style="color: #C9D1D9">      }),</span></div><div class="line"><span style="color: #C9D1D9">    )</span></div><div class="line"><span style="color: #C9D1D9">    .mutation(async (opts) =&gt; {</span></div><div class="line"><span style="color: #C9D1D9">      // Do something with the input</span></div><div class="line"><span style="color: #C9D1D9">    });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>详见 <a href="https://github.com/baselime/node-opentelemetry/blob/main/TRPC.md" target="_blank" rel="noopener noreferrer">Baselime tRPC 集成文档</a>。类似模式应适用于其他可观测性平台。</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="速率限制">速率限制<a href="https://trpc.io/zh-Hans/blog/trpc-actions#%E9%80%9F%E7%8E%87%E9%99%90%E5%88%B6" class="hash-link" aria-label="直接跳转到 速率限制" title="直接跳转到 速率限制">​</a></h3>
<p>可使用 Unkey 等服务对服务器操作进行速率限制。这是一个使用 Unkey 按用户限制请求次数的受保护操作示例：</p>
<div></div>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) class Ratelimit
import Ratelimit">Ratelimit</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@unkey/ratelimit'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">rateLimitedAction</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">protectedAction</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User;
}>(fn: MiddlewareBuilder<{
    user: User;
}, Meta, {
    user: User;
}, UnsetMarker> | MiddlewareFunction<object, Meta, {
    user: User;
}, {
    user: User;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const unkey: Ratelimit">unkey</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) new Ratelimit(config: RatelimitConfig): Ratelimit
import Ratelimit">Ratelimit</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) rootKey: string">rootKey</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var process: NodeJS.Process">process</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) NodeJS.Process.env: NodeJS.ProcessEnv">env</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="string | undefined">UNKEY_ROOT_KEY</data-lsp></span><span style="color: #D32F2F">!</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) async?: boolean | undefined">async</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">true</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) duration: number | Duration">duration</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'10s'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) limit: number">limit</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">5</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) namespace: string">namespace</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">`trpc_</span><span style="color: #D32F2F">${</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) path: string">path</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const unkey: Ratelimit">unkey</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Ratelimit.limit(identifier: string, opts?: LimitOptions): Promise<RatelimitResponse>">limit</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ctx: {
    user: User;
}">ctx</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) user: User">user</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) User.id: string">id</data-lsp>);</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">!</span><span style="color: #1976D2"><data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) success: boolean">success</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">throw</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) code: &quot;UNAUTHORIZED&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;NOT_FOUND&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'TOO_MANY_REQUESTS'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) message?: string | undefined">message</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var JSON: JSON">JSON</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)">stringify</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp>)</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    });</span></div><div class="line"><span style="color: #24292EFF">  }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) next: () => Promise<MiddlewareResult<{
    user: User;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) class Ratelimit
import Ratelimit">Ratelimit</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@unkey/ratelimit'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">rateLimitedAction</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">protectedAction</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User;
}>(fn: MiddlewareBuilder<{
    user: User;
}, Meta, {
    user: User;
}, UnsetMarker> | MiddlewareFunction<object, Meta, {
    user: User;
}, {
    user: User;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const unkey: Ratelimit">unkey</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) new Ratelimit(config: RatelimitConfig): Ratelimit
import Ratelimit">Ratelimit</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) rootKey: string">rootKey</data-lsp>: <data-lsp lsp="var process: NodeJS.Process">process</data-lsp>.<data-lsp lsp="(property) NodeJS.Process.env: NodeJS.ProcessEnv">env</data-lsp>.</span><span style="color: #79C0FF"><data-lsp lsp="string | undefined">UNKEY_ROOT_KEY</data-lsp></span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) async?: boolean | undefined">async</data-lsp>: </span><span style="color: #79C0FF">true</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) duration: number | Duration">duration</data-lsp>: </span><span style="color: #A5D6FF">'10s'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) limit: number">limit</data-lsp>: </span><span style="color: #79C0FF">5</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) namespace: string">namespace</data-lsp>: </span><span style="color: #A5D6FF">`trpc_${</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) path: string">path</data-lsp></span><span style="color: #A5D6FF">}`</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> <data-lsp lsp="const unkey: Ratelimit">unkey</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Ratelimit.limit(identifier: string, opts?: LimitOptions): Promise<RatelimitResponse>">limit</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.<data-lsp lsp="(property) ctx: {
    user: User;
}">ctx</data-lsp>.<data-lsp lsp="(property) user: User">user</data-lsp>.<data-lsp lsp="(property) User.id: string">id</data-lsp>);</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9"><data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp>.<data-lsp lsp="(property) success: boolean">success</data-lsp>) {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">throw</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) code: &quot;UNAUTHORIZED&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;NOT_FOUND&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp>: </span><span style="color: #A5D6FF">'TOO_MANY_REQUESTS'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) message?: string | undefined">message</data-lsp>: </span><span style="color: #79C0FF"><data-lsp lsp="var JSON: JSON">JSON</data-lsp></span><span style="color: #C9D1D9">.</span><span style="color: #79C0FF"><data-lsp lsp="(method) JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)">stringify</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp>),</span></div><div class="line"><span style="color: #C9D1D9">    });</span></div><div class="line"><span style="color: #C9D1D9">  }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) next: () => Promise<MiddlewareResult<{
    user: User;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="app/_actions.ts"><div class="code-title">app/_actions.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #22863A">'use server'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'zod'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import rateLimitedAction">rateLimitedAction</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'../server/trpc'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const commentOnPost: (input: {
    postId: string;
    content: string;
}) => Promise<void>">commentOnPost</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import rateLimitedAction">rateLimitedAction</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.input<z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}, {
    postId: string;
    content: string;
}, UnsetMarker, UnsetMarker, true>">input</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function object<{
    postId: z.ZodString;
    content: z.ZodString;
}>(shape?: {
    postId: z.ZodString;
    content: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) postId: z.ZodString">postId</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) content: z.ZodString">content</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  )</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, { postId: string; content: string; }, { postId: string; content: string; }, UnsetMarker, UnsetMarker, true>.mutation<void>(resolver: ProcedureResolver<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}, UnsetMarker, void>): (input: {
    postId: string;
    content: string;
}) => Promise<void>">mutation</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #1976D2"><data-lsp lsp="namespace console
var console: Console">console</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)">log</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #22863A">`</span><span style="color: #D32F2F">${</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.ctx: {
    user: User;
}">ctx</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) user: User">user</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) User.name: string">name</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A"> commented on </span><span style="color: #D32F2F">${</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.input: {
    postId: string;
    content: string;
}">input</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) postId: string">postId</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A"> saying </span><span style="color: #D32F2F">${</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.input: {
    postId: string;
    content: string;
}">input</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) content: string">content</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    );</span></div><div class="line"><span style="color: #24292EFF">  });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="app/_actions.ts"><div class="code-title">app/_actions.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #A5D6FF">'use server'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'zod'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import rateLimitedAction">rateLimitedAction</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'../server/trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const commentOnPost: (input: {
    postId: string;
    content: string;
}) => Promise<void>">commentOnPost</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(alias) const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import rateLimitedAction">rateLimitedAction</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.input<z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}, {
    postId: string;
    content: string;
}, UnsetMarker, UnsetMarker, true>">input</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function object<{
    postId: z.ZodString;
    content: z.ZodString;
}>(shape?: {
    postId: z.ZodString;
    content: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) postId: z.ZodString">postId</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) content: z.ZodString">content</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  )</span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, { postId: string; content: string; }, { postId: string; content: string; }, UnsetMarker, UnsetMarker, true>.mutation<void>(resolver: ProcedureResolver<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}, UnsetMarker, void>): (input: {
    postId: string;
    content: string;
}) => Promise<void>">mutation</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="namespace console
var console: Console">console</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)">log</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #A5D6FF">`${</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.ctx: {
    user: User;
}">ctx</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) user: User">user</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) User.name: string">name</data-lsp></span><span style="color: #A5D6FF">} commented on ${</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.input: {
    postId: string;
    content: string;
}">input</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) postId: string">postId</data-lsp></span><span style="color: #A5D6FF">} saying ${</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.input: {
    postId: string;
    content: string;
}">input</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) content: string">content</data-lsp></span><span style="color: #A5D6FF">}`</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    );</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>阅读 <a href="https://www.unkey.com/blog/ratelimit-trpc-routes" target="_blank" rel="noopener noreferrer">Unkey 团队关于 tRPC 速率限制的博文</a>获取更多信息。</p>
<p>可能性是无限的，相信你已经积累了许多优秀的实用中间件。如果还没有，不妨探索现成的 <code>npm install</code> 方案！</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="结语">结语<a href="https://trpc.io/zh-Hans/blog/trpc-actions#%E7%BB%93%E8%AF%AD" class="hash-link" aria-label="直接跳转到 结语" title="直接跳转到 结语">​</a></h2>
<p>服务器操作绝非银弹。对于需要动态数据的场景，建议保持客户端 React Query 缓存，通过 <code>useMutation</code> 执行变更。这完全合理。这些新原语支持渐进式采用，可在适当时机将现有 tRPC API 中的单个过程迁移为服务器操作，无需重写整个 API。</p>
<p>通过 tRPC 定义服务器操作，你可以复用当前大量业务逻辑，并自由选择将变更暴露为服务器操作或传统变更方式。作为开发者，你有权选择最适合应用的模式。若尚未使用 tRPC，推荐探索 <a href="https://github.com/TheEdoRan/next-safe-action" target="_blank" rel="noopener noreferrer">next-safe-action</a> 和 <a href="https://github.com/IdoPesok/zsa" target="_blank" rel="noopener noreferrer">zsa</a> 等库，它们同样提供类型安全且经过输入验证的服务器操作方案。</p>
<p>想查看实际应用案例？请访问 <a href="https://trellix-trpc.vercel.app/" target="_blank" rel="noopener noreferrer">Trellix tRPC</a>，这是我近期使用这些新原语构建的示例应用。</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="期待你的反馈">期待你的反馈<a href="https://trpc.io/zh-Hans/blog/trpc-actions#%E6%9C%9F%E5%BE%85%E4%BD%A0%E7%9A%84%E5%8F%8D%E9%A6%88" class="hash-link" aria-label="直接跳转到 期待你的反馈" title="直接跳转到 期待你的反馈">​</a></h3>
<p>你对此有何看法？欢迎在 <a href="https://github.com/trpc/trpc/discussions/5737" target="_blank" rel="noopener noreferrer">Github 讨论区</a>分享意见，帮助我们迭代优化这些基础功能。</p>
<p>目前仍有改进空间，尤其在错误处理方面。Next.js 提倡返回错误对象，我们致力于实现完全类型安全的方案。可参考 <a href="https://github.com/trpc/trpc/pull/5554" target="_blank" rel="noopener noreferrer">Alex 的 WIP PR</a> 了解早期探索。</p>
<p>下次再见，编码愉快！</p>]]></content>
        <author>
            <name>Julius Marminge</name>
            <uri>https://www.jumr.dev/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[编写微型 tRPC 客户端]]></title>
        <id>https://trpc.io/zh-Hans/blog/tinyrpc-client</id>
        <link href="https://trpc.io/zh-Hans/blog/tinyrpc-client"/>
        <updated>2023-01-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[本页面由 PageTurner AI 翻译（测试版）。未经项目官方认可。]]></summary>
        <content type="html"><![CDATA[<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>非官方测试版翻译</div><div class="admonitionContent_e2NW"><p>本页面由 <a href="https://page-turner.com/" target="_blank" rel="noopener noreferrer"><strong>PageTurner</strong></a> AI 翻译（测试版）。未经项目官方认可。
发现错误？ <a href="https://feedback.page-turner.com/?repo_id=683d130a-1828-4b22-91cd-ef2d269ef3f5&amp;file_path=blog%2F2023-01-17-tinyrpc-client.mdx&amp;locale=zh-Hans" target="_blank" rel="noopener noreferrer">报告问题 →</a></p></div></div>
<p>你是否好奇过 tRPC 的工作原理？或许你想为项目贡献代码，却被内部实现吓退？本文旨在通过编写一个覆盖 tRPC 核心机制的微型客户端，带你深入理解 tRPC 的内部运作。</p>
<!-- -->
<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>信息</div><div class="admonitionContent_e2NW"><p>建议掌握 TypeScript 的核心概念，包括泛型、条件类型、<code>extends</code> 关键字和递归。若不熟悉这些概念，推荐先学习 <a href="https://twitter.com/mattpocockuk" target="_blank" rel="noopener noreferrer">Matt Pocock</a> 的 <a href="https://github.com/total-typescript/beginners-typescript-tutorial" target="_blank" rel="noopener noreferrer">TypeScript 入门教程</a>，再继续阅读本文。</p></div></div>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="概述">概述<a href="https://trpc.io/zh-Hans/blog/tinyrpc-client#%E6%A6%82%E8%BF%B0" class="hash-link" aria-label="直接跳转到 概述" title="直接跳转到 概述">​</a></h2>
<p>假设我们有以下包含三个过程（procedure）的简易 tRPC 路由器：</p>
<div></div>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Post = {
    id: string;
    title: string;
}">Post</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> { <data-lsp lsp="(property) id: string">id</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">; <data-lsp lsp="(property) title: string">title</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF"> };</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const posts: Post[]">posts</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Post = {
    id: string;
    title: string;
}">Post</data-lsp></span><span style="color: #24292EFF">[] </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> [];</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}>>">appRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const router: RouterBuilder
<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}>(_: {
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) post: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}>>">post</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const router: RouterBuilder
<{
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}>(_: {
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) byId: QueryProcedure<{
    input: {
        id: string;
    };
    output: Post;
    meta: object;
}>">byId</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    id: string;
}, {
    id: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function object<{
    id: z.ZodString;
}>(shape?: {
    id: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) id: z.ZodString">id</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">() }))</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, { id: string; }, { id: string; }, UnsetMarker, UnsetMarker, false>.query<Post>(resolver: ProcedureResolver<object, object, object, {
    id: string;
}, UnsetMarker, Post>): QueryProcedure<{
    input: {
        id: string;
    };
    output: Post;
    meta: object;
}>">query</data-lsp></span><span style="color: #24292EFF">(({ <data-lsp lsp="(parameter) input: {
    id: string;
}">input</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const post: Post | undefined">post</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const posts: Post[]">posts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Array<Post>.find(predicate: (value: Post, index: number, obj: Post[]) => unknown, thisArg?: any): Post | undefined (+1 overload)">find</data-lsp></span><span style="color: #24292EFF">((<data-lsp lsp="(parameter) p: Post">p</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) p: Post">p</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) id: string">id</data-lsp> </span><span style="color: #D32F2F">===</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) input: {
    id: string;
}">input</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) id: string">id</data-lsp>);</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">!</span><span style="color: #24292EFF"><data-lsp lsp="const post: Post | undefined">post</data-lsp>) </span><span style="color: #D32F2F">throw</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) code: &quot;NOT_FOUND&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;UNAUTHORIZED&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">"NOT_FOUND"</span><span style="color: #24292EFF"> });</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> <data-lsp lsp="const post: Post">post</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">      })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) byTitle: QueryProcedure<{
    input: {
        title: string;
    };
    output: Post;
    meta: object;
}>">byTitle</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) title: z.ZodString">title</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">() }))</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, false>.query<Post>(resolver: ProcedureResolver<object, object, object, {
    title: string;
}, UnsetMarker, Post>): QueryProcedure<{
    input: {
        title: string;
    };
    output: Post;
    meta: object;
}>">query</data-lsp></span><span style="color: #24292EFF">(({ <data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const post: Post | undefined">post</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const posts: Post[]">posts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Array<Post>.find(predicate: (value: Post, index: number, obj: Post[]) => unknown, thisArg?: any): Post | undefined (+1 overload)">find</data-lsp></span><span style="color: #24292EFF">((<data-lsp lsp="(parameter) p: Post">p</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) p: Post">p</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) title: string">title</data-lsp> </span><span style="color: #D32F2F">===</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) title: string">title</data-lsp>);</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">!</span><span style="color: #24292EFF"><data-lsp lsp="const post: Post | undefined">post</data-lsp>) </span><span style="color: #D32F2F">throw</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) code: &quot;NOT_FOUND&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;UNAUTHORIZED&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">"NOT_FOUND"</span><span style="color: #24292EFF"> });</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> <data-lsp lsp="const post: Post">post</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">      })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) create: MutationProcedure<{
    input: {
        title: string;
    };
    output: {
        title: string;
        id: string;
    };
    meta: object;
}>">create</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) title: z.ZodString">title</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">() }))</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, false>.mutation<{
    title: string;
    id: string;
}>(resolver: ProcedureResolver<object, object, object, {
    title: string;
}, UnsetMarker, {
    title: string;
    id: string;
}>): MutationProcedure<{
    input: {
        title: string;
    };
    output: {
        title: string;
        id: string;
    };
    meta: object;
}>">mutation</data-lsp></span><span style="color: #24292EFF">(({ <data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> { <data-lsp lsp="(property) id: string">id</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const uuid: () => string">uuid</data-lsp></span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">...</span><span style="color: #24292EFF"><data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp> };</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #1976D2"><data-lsp lsp="const posts: Post[]">posts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Array<Post>.push(...items: Post[]): number">push</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp>);</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> <data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">      })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Post = {
    id: string;
    title: string;
}">Post</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> { </span><span style="color: #FFA657"><data-lsp lsp="(property) id: string">id</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">; </span><span style="color: #FFA657"><data-lsp lsp="(property) title: string">title</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9"> };</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const posts: Post[]">posts</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Post = {
    id: string;
    title: string;
}">Post</data-lsp></span><span style="color: #C9D1D9">[] </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> [];</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}>>">appRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="const router: RouterBuilder
<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}>(_: {
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) post: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}>>">post</data-lsp>: </span><span style="color: #D2A8FF"><data-lsp lsp="const router: RouterBuilder
<{
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}>(_: {
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) byId: QueryProcedure<{
    input: {
        id: string;
    };
    output: Post;
    meta: object;
}>">byId</data-lsp>: <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    id: string;
}, {
    id: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function object<{
    id: z.ZodString;
}>(shape?: {
    id: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) id: z.ZodString">id</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">() }))</span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, { id: string; }, { id: string; }, UnsetMarker, UnsetMarker, false>.query<Post>(resolver: ProcedureResolver<object, object, object, {
    id: string;
}, UnsetMarker, Post>): QueryProcedure<{
    input: {
        id: string;
    };
    output: Post;
    meta: object;
}>">query</data-lsp></span><span style="color: #C9D1D9">(({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: {
    id: string;
}">input</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const post: Post | undefined">post</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const posts: Post[]">posts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Array<Post>.find(predicate: (value: Post, index: number, obj: Post[]) => unknown, thisArg?: any): Post | undefined (+1 overload)">find</data-lsp></span><span style="color: #C9D1D9">((</span><span style="color: #FFA657"><data-lsp lsp="(parameter) p: Post">p</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) p: Post">p</data-lsp>.<data-lsp lsp="(property) id: string">id</data-lsp> </span><span style="color: #FF7B72">===</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) input: {
    id: string;
}">input</data-lsp>.<data-lsp lsp="(property) id: string">id</data-lsp>);</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9"><data-lsp lsp="const post: Post | undefined">post</data-lsp>) </span><span style="color: #FF7B72">throw</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) code: &quot;NOT_FOUND&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;UNAUTHORIZED&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp>: </span><span style="color: #A5D6FF">"NOT_FOUND"</span><span style="color: #C9D1D9"> });</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="const post: Post">post</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">      }),</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) byTitle: QueryProcedure<{
    input: {
        title: string;
    };
    output: Post;
    meta: object;
}>">byTitle</data-lsp>: <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) title: z.ZodString">title</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">() }))</span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, false>.query<Post>(resolver: ProcedureResolver<object, object, object, {
    title: string;
}, UnsetMarker, Post>): QueryProcedure<{
    input: {
        title: string;
    };
    output: Post;
    meta: object;
}>">query</data-lsp></span><span style="color: #C9D1D9">(({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const post: Post | undefined">post</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const posts: Post[]">posts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Array<Post>.find(predicate: (value: Post, index: number, obj: Post[]) => unknown, thisArg?: any): Post | undefined (+1 overload)">find</data-lsp></span><span style="color: #C9D1D9">((</span><span style="color: #FFA657"><data-lsp lsp="(parameter) p: Post">p</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) p: Post">p</data-lsp>.<data-lsp lsp="(property) title: string">title</data-lsp> </span><span style="color: #FF7B72">===</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp>.<data-lsp lsp="(property) title: string">title</data-lsp>);</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9"><data-lsp lsp="const post: Post | undefined">post</data-lsp>) </span><span style="color: #FF7B72">throw</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) code: &quot;NOT_FOUND&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;UNAUTHORIZED&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp>: </span><span style="color: #A5D6FF">"NOT_FOUND"</span><span style="color: #C9D1D9"> });</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="const post: Post">post</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">      }),</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) create: MutationProcedure<{
    input: {
        title: string;
    };
    output: {
        title: string;
        id: string;
    };
    meta: object;
}>">create</data-lsp>: <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) title: z.ZodString">title</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">() }))</span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, false>.mutation<{
    title: string;
    id: string;
}>(resolver: ProcedureResolver<object, object, object, {
    title: string;
}, UnsetMarker, {
    title: string;
    id: string;
}>): MutationProcedure<{
    input: {
        title: string;
    };
    output: {
        title: string;
        id: string;
    };
    meta: object;
}>">mutation</data-lsp></span><span style="color: #C9D1D9">(({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> { <data-lsp lsp="(property) id: string">id</data-lsp>: </span><span style="color: #D2A8FF"><data-lsp lsp="const uuid: () => string">uuid</data-lsp></span><span style="color: #C9D1D9">(), </span><span style="color: #FF7B72">...</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp> };</span></div><div class="line"><span style="color: #C9D1D9">        <data-lsp lsp="const posts: Post[]">posts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Array<Post>.push(...items: Post[]): number">push</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp>);</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">      }),</span></div><div class="line"><span style="color: #C9D1D9">  }),</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>客户端的目标是在前端复现这个对象结构，从而能够这样调用过程：</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">post1</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">post</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">byId</span><span style="color: #6F42C1">.query</span><span style="color: #24292EFF">({ id</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'123'</span><span style="color: #24292EFF"> });</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">post2</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">post</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">byTitle</span><span style="color: #6F42C1">.query</span><span style="color: #24292EFF">({ title</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'Hello world'</span><span style="color: #24292EFF"> });</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">newPost</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">post</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">create</span><span style="color: #6F42C1">.mutate</span><span style="color: #24292EFF">({ title</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'Foo'</span><span style="color: #24292EFF"> });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">post1</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> client.post.byId.</span><span style="color: #D2A8FF">query</span><span style="color: #C9D1D9">({ id: </span><span style="color: #A5D6FF">'123'</span><span style="color: #C9D1D9"> });</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">post2</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> client.post.byTitle.</span><span style="color: #D2A8FF">query</span><span style="color: #C9D1D9">({ title: </span><span style="color: #A5D6FF">'Hello world'</span><span style="color: #C9D1D9"> });</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">newPost</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> client.post.create.</span><span style="color: #D2A8FF">mutate</span><span style="color: #C9D1D9">({ title: </span><span style="color: #A5D6FF">'Foo'</span><span style="color: #C9D1D9"> });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>为此，tRPC 结合了 <a href="https://mdn.io/proxy" target="_blank" rel="noopener noreferrer"><code>Proxy</code></a> 对象和 TypeScript 魔法，在对象结构上动态添加 <code>.query</code> 和 <code>.mutate</code> 方法 —— 这意味着我们实际上"欺骗"了开发者（后文详述），只为提供极致的开发体验！</p>
<p>高层实现思路是：将 <code>post.byId.query()</code> 映射为向服务器发送 GET 请求，将 <code>post.create.mutate()</code> 映射为 POST 请求，并确保类型从后端完整传播到前端。具体如何实现呢？</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="实现微型-trpc-客户端">实现微型 tRPC 客户端<a href="https://trpc.io/zh-Hans/blog/tinyrpc-client#%E5%AE%9E%E7%8E%B0%E5%BE%AE%E5%9E%8B-trpc-%E5%AE%A2%E6%88%B7%E7%AB%AF" class="hash-link" aria-label="直接跳转到 实现微型 tRPC 客户端" title="直接跳转到 实现微型 tRPC 客户端">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="️-typescript-魔法">🧙‍♀️ TypeScript 魔法<a href="https://trpc.io/zh-Hans/blog/tinyrpc-client#%EF%B8%8F-typescript-%E9%AD%94%E6%B3%95" class="hash-link" aria-label="直接跳转到 🧙‍♀️ TypeScript 魔法" title="直接跳转到 🧙‍♀️ TypeScript 魔法">​</a></h3>
<p>让我们从有趣的 TypeScript 魔法开始，解锁大家在使用 tRPC 时熟悉的自动补全和类型安全能力。</p>
<p>需要使用递归类型来推断任意深度的路由结构。同时，我们需要为过程 <code>post.byId</code> 和 <code>post.create</code> 分别添加 <code>.query</code> 和 <code>.mutate</code> 方法 —— 在 tRPC 中，这称为"装饰"过程。通过 <code>@trpc/server</code> 的推断工具，可以获取已解析方法的输入输出类型，我们将利用这些工具来推断函数类型。现在开始编码吧！</p>
<p>为实现路径自动补全和过程输入输出类型推断，我们需要：</p>
<ul>
<li>
<p>当处于路由器时，能访问其子路由器和过程（稍后详解）</p>
</li>
<li>
<p>当处于查询过程时，能调用其 <code>.query</code> 方法</p>
</li>
<li>
<p>当处于变更过程时，能调用其 <code>.mutate</code> 方法</p>
</li>
<li>
<p>当尝试访问不存在的过程时，触发类型错误提示</p>
</li>
</ul>
<p>为此我们创建以下类型：</p>
<div></div>
<div></div>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type DecorateProcedure<TProcedure> = TProcedure extends AnyTRPCQueryProcedure ? {
    query: Resolver<TProcedure>;
} : TProcedure extends AnyTRPCMutationProcedure ? {
    mutate: Resolver<TProcedure>;
} : never">DecorateProcedure</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AnyTRPCQueryProcedure = QueryProcedure<any>
import AnyTRPCQueryProcedure">AnyTRPCQueryProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) query: Resolver<TProcedure>">query</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF">&gt;;</span></div><div class="line"><span style="color: #24292EFF">    }</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AnyTRPCMutationProcedure = MutationProcedure<any>
import AnyTRPCMutationProcedure">AnyTRPCMutationProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) mutate: Resolver<TProcedure>">mutate</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF">&gt;;</span></div><div class="line"><span style="color: #24292EFF">    }</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">never</span><span style="color: #24292EFF">;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type DecorateProcedure<TProcedure> = TProcedure extends AnyTRPCQueryProcedure ? {
    query: Resolver<TProcedure>;
} : TProcedure extends AnyTRPCMutationProcedure ? {
    mutate: Resolver<TProcedure>;
} : never">DecorateProcedure</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type AnyTRPCQueryProcedure = QueryProcedure<any>
import AnyTRPCQueryProcedure">AnyTRPCQueryProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FFA657"><data-lsp lsp="(property) query: Resolver<TProcedure>">query</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9">&gt;;</span></div><div class="line"><span style="color: #C9D1D9">    }</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type AnyTRPCMutationProcedure = MutationProcedure<any>
import AnyTRPCMutationProcedure">AnyTRPCMutationProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FFA657"><data-lsp lsp="(property) mutate: Resolver<TProcedure>">mutate</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9">&gt;;</span></div><div class="line"><span style="color: #C9D1D9">    }</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">never</span><span style="color: #C9D1D9">;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>使用 tRPC 内置的推断工具获取过程输入输出类型，定义 <code>Resolver</code> 类型。</p>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(alias) type inferProcedureInput<TProcedure extends AnyProcedure> = undefined extends inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] ? void | inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] : inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;]
import inferProcedureInput">inferProcedureInput</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(alias) type inferProcedureOutput<TProcedure> = inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;output&quot;]
import inferProcedureOutput">inferProcedureOutput</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(alias) type AnyTRPCQueryProcedure = QueryProcedure<any>
import AnyTRPCQueryProcedure">AnyTRPCQueryProcedure</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(alias) type AnyTRPCMutationProcedure = MutationProcedure<any>
import AnyTRPCMutationProcedure">AnyTRPCMutationProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">} </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp></span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> (</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(parameter) input: inferProcedureInput<TProcedure>">input</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type inferProcedureInput<TProcedure extends AnyProcedure> = undefined extends inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] ? void | inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] : inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;]
import inferProcedureInput">inferProcedureInput</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF">&gt;</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Promise<T>">Promise</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(alias) type inferProcedureOutput<TProcedure> = inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;output&quot;]
import inferProcedureOutput">inferProcedureOutput</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF">&gt;&gt;;</span></div><div class="line">&nbsp;</div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(alias) type inferProcedureInput<TProcedure extends AnyProcedure> = undefined extends inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] ? void | inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] : inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;]
import inferProcedureInput">inferProcedureInput</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(alias) type inferProcedureOutput<TProcedure> = inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;output&quot;]
import inferProcedureOutput">inferProcedureOutput</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(alias) type AnyTRPCQueryProcedure = QueryProcedure<any>
import AnyTRPCQueryProcedure">AnyTRPCQueryProcedure</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(alias) type AnyTRPCMutationProcedure = MutationProcedure<any>
import AnyTRPCMutationProcedure">AnyTRPCMutationProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">} </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp></span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> (</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: inferProcedureInput<TProcedure>">input</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type inferProcedureInput<TProcedure extends AnyProcedure> = undefined extends inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] ? void | inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] : inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;]
import inferProcedureInput">inferProcedureInput</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9">&gt;,</span></div><div class="line"><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Promise<T>">Promise</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(alias) type inferProcedureOutput<TProcedure> = inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;output&quot;]
import inferProcedureOutput">inferProcedureOutput</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9">&gt;&gt;;</span></div><div class="line">&nbsp;</div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>在 <code>post.byId</code> 过程上测试：</p>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type PostById = (input: {
    id: string;
}) => Promise<Post>" style="border-bottom: solid 2px lightgrey;">PostById</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
    }>>;
}>> &amp; DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
    }>>;
}>">AppRouter</data-lsp></span><span style="color: #24292EFF">[</span><span style="color: #22863A">'post'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'byId'</span><span style="color: #24292EFF">]&gt;;</span></div><div class="meta-line"><span class="popover-prefix">        </span><span class="popover"><div class="arrow"></div>type PostById = (input: {
    id: string;
}) =&gt; Promise&lt;Post&gt;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type PostById = (input: {
    id: string;
}) => Promise<Post>" style="border-bottom: solid 2px lightgrey;">PostById</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
    }>>;
}>> &amp; DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
    }>>;
}>">AppRouter</data-lsp></span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'post'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'byId'</span><span style="color: #C9D1D9">]&gt;;</span></div><div class="meta-line"><span class="popover-prefix">        </span><span class="popover"><div class="arrow"></div>type PostById = (input: {
    id: string;
}) =&gt; Promise&lt;Post&gt;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>完美符合预期！现在可以在过程上调用 <code>.query</code> 并获取正确的输入输出类型推断！</p>
<p>最后创建递归遍历路由器的类型，并装饰所有过程：</p>
<div></div>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">"@trpc/server"</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) type AnyTRPCRouter = Router<any, any>
import AnyTRPCRouter">AnyTRPCRouter</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">"@trpc/server"</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp></span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  [</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TKey">TKey</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">in</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">keyof</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #24292EFF">]</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #24292EFF">[</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TKey">TKey</data-lsp></span><span style="color: #24292EFF">] </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">infer</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp></span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #24292EFF">&gt;</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type DecorateProcedure<TProcedure> = TProcedure extends AnyTRPCQueryProcedure ? {
    query: Resolver<TProcedure>;
} : TProcedure extends AnyTRPCMutationProcedure ? {
    mutate: Resolver<TProcedure>;
} : never">DecorateProcedure</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #24292EFF">&gt;</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">never</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">never</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">};</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">"@trpc/server"</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) type AnyTRPCRouter = Router<any, any>
import AnyTRPCRouter">AnyTRPCRouter</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">"@trpc/server"</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp></span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  [</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TKey">TKey</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">in</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">keyof</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #C9D1D9">]</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #C9D1D9">[</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TKey">TKey</data-lsp></span><span style="color: #C9D1D9">] </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">infer</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #C9D1D9">&gt;</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type DecorateProcedure<TProcedure> = TProcedure extends AnyTRPCQueryProcedure ? {
    query: Resolver<TProcedure>;
} : TProcedure extends AnyTRPCMutationProcedure ? {
    mutate: Resolver<TProcedure>;
} : never">DecorateProcedure</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #C9D1D9">&gt;</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">never</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">never</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">};</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>解析这个类型：</p>
<ol>
<li>
<p>泛型参数 <code>TRPCRouterRecord</code> 包含 tRPC 路由器上的所有过程和子路由器</p>
</li>
<li>
<p>我们遍历记录中的键（即过程或路由器名称），并执行以下操作：</p>
<ul>
<li>如果键映射到路由器，则递归调用该路由器过程记录上的类型，这将装饰该路由器中的所有过程。在遍历路径时，这能提供自动补全功能。</li>
<li>如果键映射到过程，则使用之前创建的<code>DecorateProcedure</code>类型装饰该过程。</li>
<li>如果键不映射到任何过程或路由器，则分配<code>never</code>类型（相当于声明"此键不存在"），尝试访问时将触发类型错误。</li>
</ul>
</li>
</ol>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="-代理重映射">🤯 代理重映射<a href="https://trpc.io/zh-Hans/blog/tinyrpc-client#-%E4%BB%A3%E7%90%86%E9%87%8D%E6%98%A0%E5%B0%84" class="hash-link" aria-label="直接跳转到 🤯 代理重映射" title="直接跳转到 🤯 代理重映射">​</a></h3>
<p>设置好所有类型后，我们需要实际实现功能：在客户端增强服务器路由定义的结构，从而能像调用普通函数一样调用过程。</p>
<p>首先创建用于生成递归代理的辅助函数——<code>createRecursiveProxy</code>：</p>
<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>信息</div><div class="admonitionContent_e2NW"><p>这与生产环境实现几乎完全相同（仅未处理某些边缘情况）。<a href="https://github.com/trpc/trpc/blob/main/packages/server/src/shared/createProxy/index.ts" target="_blank" rel="noopener noreferrer">查看源码</a>！</p></div></div>
<div></div>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">interface</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface ProxyCallbackOptions">ProxyCallbackOptions</data-lsp></span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">readonly</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">[];</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">readonly</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">unknown</span><span style="color: #24292EFF">[];</span></div><div class="line"><span style="color: #24292EFF">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type ProxyCallback = (opts: ProxyCallbackOptions) => unknown">ProxyCallback</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface ProxyCallbackOptions">ProxyCallbackOptions</data-lsp></span><span style="color: #24292EFF">) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">unknown</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">function</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(parameter) callback: ProxyCallback">callback</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type ProxyCallback = (opts: ProxyCallbackOptions) => unknown">ProxyCallback</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) path: readonly string[]">path</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">readonly</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">[]) {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const proxy: unknown">proxy</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">unknown</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var Proxy: ProxyConstructor
new <() => void>(target: () => void, handler: ProxyHandler<() => void>) => () => void">Proxy</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    () </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #C2C3C5">// dummy no-op function since we don't have any</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #C2C3C5">// client-side target we want to remap to</span></div><div class="line"><span style="color: #24292EFF">    }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1"><data-lsp lsp="(method) ProxyHandler<() => void>.get?(target: () => void, p: string | symbol, receiver: any): any">get</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(parameter) _obj: () => void">_obj</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) key: string | symbol">key</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">typeof</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) key: string | symbol">key</data-lsp> </span><span style="color: #D32F2F">!==</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'string'</span><span style="color: #24292EFF">) </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #C2C3C5">// Recursively compose the full path until a function is invoked</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(parameter) callback: ProxyCallback">callback</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">...</span><span style="color: #24292EFF"><data-lsp lsp="(parameter) path: readonly string[]">path</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) key: string">key</data-lsp>]);</span></div><div class="line"><span style="color: #24292EFF">      }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1"><data-lsp lsp="(method) ProxyHandler<() => void>.apply?(target: () => void, thisArg: any, argArray: any[]): any">apply</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(parameter) _1: () => void">_1</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) _2: any">_2</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) args: any[]">args</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #C2C3C5">// Call the callback function with the entire path we</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #C2C3C5">// recursively created and forward the arguments</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(parameter) callback: (opts: ProxyCallbackOptions) => unknown">callback</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">          <data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">          <data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">        });</span></div><div class="line"><span style="color: #24292EFF">      }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  );</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> <data-lsp lsp="const proxy: unknown">proxy</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">interface</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface ProxyCallbackOptions">ProxyCallbackOptions</data-lsp></span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">readonly</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">[];</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">readonly</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">unknown</span><span style="color: #C9D1D9">[];</span></div><div class="line"><span style="color: #C9D1D9">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type ProxyCallback = (opts: ProxyCallbackOptions) => unknown">ProxyCallback</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface ProxyCallbackOptions">ProxyCallbackOptions</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">unknown</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">function</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FFA657"><data-lsp lsp="(parameter) callback: ProxyCallback">callback</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type ProxyCallback = (opts: ProxyCallbackOptions) => unknown">ProxyCallback</data-lsp></span><span style="color: #C9D1D9">, </span><span style="color: #FFA657"><data-lsp lsp="(parameter) path: readonly string[]">path</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">readonly</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">[]) {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const proxy: unknown">proxy</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">unknown</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var Proxy: ProxyConstructor
new <() => void>(target: () => void, handler: ProxyHandler<() => void>) => () => void">Proxy</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    () </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #8B949E">// dummy no-op function since we don't have any</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #8B949E">// client-side target we want to remap to</span></div><div class="line"><span style="color: #C9D1D9">    },</span></div><div class="line"><span style="color: #C9D1D9">    {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProxyHandler<() => void>.get?(target: () => void, p: string | symbol, receiver: any): any">get</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FFA657"><data-lsp lsp="(parameter) _obj: () => void">_obj</data-lsp></span><span style="color: #C9D1D9">, </span><span style="color: #FFA657"><data-lsp lsp="(parameter) key: string | symbol">key</data-lsp></span><span style="color: #C9D1D9">) {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">typeof</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) key: string | symbol">key</data-lsp> </span><span style="color: #FF7B72">!==</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'string'</span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #8B949E">// Recursively compose the full path until a function is invoked</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="(parameter) callback: ProxyCallback">callback</data-lsp>, [</span><span style="color: #FF7B72">...</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) path: readonly string[]">path</data-lsp>, <data-lsp lsp="(parameter) key: string">key</data-lsp>]);</span></div><div class="line"><span style="color: #C9D1D9">      },</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProxyHandler<() => void>.apply?(target: () => void, thisArg: any, argArray: any[]): any">apply</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FFA657"><data-lsp lsp="(parameter) _1: () => void">_1</data-lsp></span><span style="color: #C9D1D9">, </span><span style="color: #FFA657"><data-lsp lsp="(parameter) _2: any">_2</data-lsp></span><span style="color: #C9D1D9">, </span><span style="color: #FFA657"><data-lsp lsp="(parameter) args: any[]">args</data-lsp></span><span style="color: #C9D1D9">) {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #8B949E">// Call the callback function with the entire path we</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #8B949E">// recursively created and forward the arguments</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(parameter) callback: (opts: ProxyCallbackOptions) => unknown">callback</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">          <data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">          <data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">        });</span></div><div class="line"><span style="color: #C9D1D9">      },</span></div><div class="line"><span style="color: #C9D1D9">    },</span></div><div class="line"><span style="color: #C9D1D9">  );</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="const proxy: unknown">proxy</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>这看起来有些神奇，它究竟做了什么？</p>
<ul>
<li>
<p><code>get</code>方法处理属性访问（如 <code>post.byId</code>）。key即访问的属性名——访问<code>post</code>时<code>key</code>为<code>post</code>，访问<code>post.byId</code>时<code>key</code>为<code>byId</code>。递归代理将所有键组合成最终路径（例如["post", "byId", "query"]），用于确定请求的目标URL。</p>
</li>
<li>
<p><code>apply</code>方法在调用代理函数时触发（如<code>.query(args)</code>）。<code>args</code>即传入函数的参数——调用<code>post.byId.query(args)</code>时，<code>args</code>即输入数据（根据过程类型作为查询参数或请求体传递）。<code>createRecursiveProxy</code>接收的回调函数将映射<code>apply</code>操作，处理路径和参数。</p>
</li>
</ul>
<p>下图展示了调用<code>trpc.post.byId.query({ id: 1 })</code>时代理的工作流程：</p>
<p><img decoding="async" loading="lazy" src="https://assets.trpc.io/www/blog/2023-01-17-tinyrpc-client/proxy.png" alt="代理" class="img_Njog"></p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="-整合实现">🧩 整合实现<a href="https://trpc.io/zh-Hans/blog/tinyrpc-client#-%E6%95%B4%E5%90%88%E5%AE%9E%E7%8E%B0" class="hash-link" aria-label="直接跳转到 🧩 整合实现" title="直接跳转到 🧩 整合实现">​</a></h3>
<p>有了这个辅助函数并理解其原理后，我们用它创建客户端。为<code>createRecursiveProxy</code>提供回调函数，该函数将获取路径和参数，通过<code>fetch</code>向服务器发起请求。需要为函数添加泛型以支持任意tRPC路由器类型（<code>AnyTRPCRouter</code>），然后将返回类型断言为我们之前创建的<code>DecorateRouterRecord</code>类型：</p>
<div></div>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) type TRPCResponse<TData = unknown, TError extends TRPCErrorShape = TRPCErrorShape<object>> = TRPCErrorResponse<TError> | TRPCSuccessResponse<TData>
import TRPCResponse">TRPCResponse</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server/rpc'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const createTinyRPCClient: <TRouter extends AnyTRPCRouter>(baseUrl: string) => DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">createTinyRPCClient</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> &lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TRouter in <TRouter extends AnyTRPCRouter>(baseUrl: string): DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">TRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AnyTRPCRouter = Router<any, any>
import AnyTRPCRouter">AnyTRPCRouter</data-lsp></span><span style="color: #24292EFF">&gt;(</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(parameter) baseUrl: string">baseUrl</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">) </span><span style="color: #D32F2F">=&gt;</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const path: string[]">path</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">...</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp>]; </span><span style="color: #C2C3C5">// e.g. ["post", "byId", "query"]</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const path: string[]">path</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Array<string>.pop(): string | undefined">pop</data-lsp></span><span style="color: #24292EFF">()</span><span style="color: #D32F2F">!</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'query'</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'mutate'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const dotPath: string">dotPath</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const path: string[]">path</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Array<string>.join(separator?: string): string">join</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #22863A">'.'</span><span style="color: #24292EFF">); </span><span style="color: #C2C3C5">// "post.byId" - this is the path procedures have on the backend</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">let</span><span style="color: #24292EFF"> <data-lsp lsp="let uri: string">uri</data-lsp> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">`</span><span style="color: #D32F2F">${</span><span style="color: #24292EFF"><data-lsp lsp="(parameter) baseUrl: string">baseUrl</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A">/</span><span style="color: #D32F2F">${</span><span style="color: #24292EFF"><data-lsp lsp="const dotPath: string">dotPath</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> [</span><span style="color: #1976D2"><data-lsp lsp="const input: unknown">input</data-lsp></span><span style="color: #24292EFF">] </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const stringifiedInput: string | false">stringifiedInput</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> <data-lsp lsp="const input: unknown">input</data-lsp> </span><span style="color: #D32F2F">!==</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">&amp;&amp;</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var JSON: JSON">JSON</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)">stringify</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="const input: {} | null">input</data-lsp>);</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">let</span><span style="color: #24292EFF"> <data-lsp lsp="let body: string | undefined">body</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">undefined</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (<data-lsp lsp="const stringifiedInput: string | false">stringifiedInput</data-lsp> </span><span style="color: #D32F2F">!==</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">false</span><span style="color: #24292EFF">) {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (<data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp> </span><span style="color: #D32F2F">===</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'query'</span><span style="color: #24292EFF">) {</span></div><div class="line"><span style="color: #24292EFF">        <data-lsp lsp="let uri: string">uri</data-lsp> </span><span style="color: #D32F2F">+=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">`?input=</span><span style="color: #D32F2F">${</span><span style="color: #6F42C1"><data-lsp lsp="function encodeURIComponent(uriComponent: string | number | boolean): string">encodeURIComponent</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="const stringifiedInput: string">stringifiedInput</data-lsp>)</span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">      } </span><span style="color: #D32F2F">else</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        <data-lsp lsp="let body: string | undefined">body</data-lsp> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> <data-lsp lsp="const stringifiedInput: string">stringifiedInput</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">      }</span></div><div class="line"><span style="color: #24292EFF">    }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const json: TRPCResponse">json</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type TRPCResponse<TData = unknown, TError extends TRPCErrorShape = TRPCErrorShape<object>> = TRPCErrorResponse<TError> | TRPCSuccessResponse<TData>
import TRPCResponse">TRPCResponse</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>">fetch</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="let uri: string">uri</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) RequestInit.method?: string | undefined">method</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp> </span><span style="color: #D32F2F">===</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'query'</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'GET'</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'POST'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) RequestInit.headers?: HeadersInit | undefined">headers</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #22863A">'Content-Type'</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'application/json'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) RequestInit.body?: BodyInit | null | undefined">body</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #6F42C1">.<data-lsp lsp="(method) Promise<Response>.then<any, TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown>>(onfulfilled?: ((value: Response) => any) | null | undefined, onrejected?: ((reason: any) => TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown> | PromiseLike<TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown>>) | null | undefined): Promise<...>">then</data-lsp></span><span style="color: #24292EFF">((<data-lsp lsp="(parameter) res: Response">res</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) res: Response">res</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Body.json(): Promise<any>">json</data-lsp></span><span style="color: #24292EFF">());</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #22863A">'error'</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">in</span><span style="color: #24292EFF"> <data-lsp lsp="const json: TRPCResponse">json</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">throw</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var Error: ErrorConstructor
new (message?: string) => Error">Error</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #22863A">`Error: </span><span style="color: #D32F2F">${</span><span style="color: #1976D2"><data-lsp lsp="const json: TRPCErrorResponse<TRPCErrorShape<object>>">json</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) JSONRPC2.ErrorResponse<TRPCErrorShape<object>>.error: TRPCErrorShape<object>">error</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) TRPCErrorShape<object>.message: string">message</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #24292EFF">);</span></div><div class="line"><span style="color: #24292EFF">    }</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #C2C3C5">// No error - all good. Return the data.</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const json: TRPCSuccessResponse<unknown>">json</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) JSONRPC2.ResultResponse<TRPCResult<unknown>>.result: TRPCResult<unknown>">result</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) TRPCResult<unknown>.data: unknown">data</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">  }</span><span style="color: #212121">,</span><span style="color: #24292EFF"> []) </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TRouter in <TRouter extends AnyTRPCRouter>(baseUrl: string): DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">TRouter</data-lsp></span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">]&gt;;</span></div><div class="line"><span style="color: #C2C3C5">//   ^? provide empty array as path to begin with</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) type TRPCResponse<TData = unknown, TError extends TRPCErrorShape = TRPCErrorShape<object>> = TRPCErrorResponse<TError> | TRPCSuccessResponse<TData>
import TRPCResponse">TRPCResponse</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server/rpc'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="const createTinyRPCClient: <TRouter extends AnyTRPCRouter>(baseUrl: string) => DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">createTinyRPCClient</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> &lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TRouter in <TRouter extends AnyTRPCRouter>(baseUrl: string): DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">TRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type AnyTRPCRouter = Router<any, any>
import AnyTRPCRouter">AnyTRPCRouter</data-lsp></span><span style="color: #C9D1D9">&gt;(</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(parameter) baseUrl: string">baseUrl</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #D2A8FF"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const path: string[]">path</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> [</span><span style="color: #FF7B72">...</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp>.<data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp>]; </span><span style="color: #8B949E">// e.g. ["post", "byId", "query"]</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const path: string[]">path</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Array<string>.pop(): string | undefined">pop</data-lsp></span><span style="color: #C9D1D9">()</span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'query'</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">|</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'mutate'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const dotPath: string">dotPath</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const path: string[]">path</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Array<string>.join(separator?: string): string">join</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'.'</span><span style="color: #C9D1D9">); </span><span style="color: #8B949E">// "post.byId" - this is the path procedures have on the backend</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">let</span><span style="color: #C9D1D9"> <data-lsp lsp="let uri: string">uri</data-lsp> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">`${</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) baseUrl: string">baseUrl</data-lsp></span><span style="color: #A5D6FF">}/${</span><span style="color: #C9D1D9"><data-lsp lsp="const dotPath: string">dotPath</data-lsp></span><span style="color: #A5D6FF">}`</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> [</span><span style="color: #79C0FF"><data-lsp lsp="const input: unknown">input</data-lsp></span><span style="color: #C9D1D9">] </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp>.<data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const stringifiedInput: string | false">stringifiedInput</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const input: unknown">input</data-lsp> </span><span style="color: #FF7B72">!==</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">&amp;&amp;</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var JSON: JSON">JSON</data-lsp></span><span style="color: #C9D1D9">.</span><span style="color: #79C0FF"><data-lsp lsp="(method) JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)">stringify</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="const input: {} | null">input</data-lsp>);</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">let</span><span style="color: #C9D1D9"> <data-lsp lsp="let body: string | undefined">body</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">undefined</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">|</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (<data-lsp lsp="const stringifiedInput: string | false">stringifiedInput</data-lsp> </span><span style="color: #FF7B72">!==</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">false</span><span style="color: #C9D1D9">) {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (<data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp> </span><span style="color: #FF7B72">===</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'query'</span><span style="color: #C9D1D9">) {</span></div><div class="line"><span style="color: #C9D1D9">        <data-lsp lsp="let uri: string">uri</data-lsp> </span><span style="color: #FF7B72">+=</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">`?input=${</span><span style="color: #79C0FF"><data-lsp lsp="function encodeURIComponent(uriComponent: string | number | boolean): string">encodeURIComponent</data-lsp></span><span style="color: #A5D6FF">(</span><span style="color: #C9D1D9"><data-lsp lsp="const stringifiedInput: string">stringifiedInput</data-lsp></span><span style="color: #A5D6FF">)</span><span style="color: #A5D6FF">}`</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">      } </span><span style="color: #FF7B72">else</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        <data-lsp lsp="let body: string | undefined">body</data-lsp> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const stringifiedInput: string">stringifiedInput</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">      }</span></div><div class="line"><span style="color: #C9D1D9">    }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const json: TRPCResponse">json</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type TRPCResponse<TData = unknown, TError extends TRPCErrorShape = TRPCErrorShape<object>> = TRPCErrorResponse<TError> | TRPCSuccessResponse<TData>
import TRPCResponse">TRPCResponse</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>">fetch</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="let uri: string">uri</data-lsp>, {</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) RequestInit.method?: string | undefined">method</data-lsp>: <data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp> </span><span style="color: #FF7B72">===</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'query'</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'GET'</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'POST'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) RequestInit.headers?: HeadersInit | undefined">headers</data-lsp>: {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #A5D6FF">'Content-Type'</span><span style="color: #C9D1D9">: </span><span style="color: #A5D6FF">'application/json'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">      },</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) RequestInit.body?: BodyInit | null | undefined">body</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">    }).</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Promise<Response>.then<any, TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown>>(onfulfilled?: ((value: Response) => any) | null | undefined, onrejected?: ((reason: any) => TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown> | PromiseLike<TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown>>) | null | undefined): Promise<...>">then</data-lsp></span><span style="color: #C9D1D9">((</span><span style="color: #FFA657"><data-lsp lsp="(parameter) res: Response">res</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) res: Response">res</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Body.json(): Promise<any>">json</data-lsp></span><span style="color: #C9D1D9">());</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #A5D6FF">'error'</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">in</span><span style="color: #C9D1D9"> <data-lsp lsp="const json: TRPCResponse">json</data-lsp>) {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">throw</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var Error: ErrorConstructor
new (message?: string) => Error">Error</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">`Error: ${</span><span style="color: #C9D1D9"><data-lsp lsp="const json: TRPCErrorResponse<TRPCErrorShape<object>>">json</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) JSONRPC2.ErrorResponse<TRPCErrorShape<object>>.error: TRPCErrorShape<object>">error</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) TRPCErrorShape<object>.message: string">message</data-lsp></span><span style="color: #A5D6FF">}`</span><span style="color: #C9D1D9">);</span></div><div class="line"><span style="color: #C9D1D9">    }</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #8B949E">// No error - all good. Return the data.</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="const json: TRPCSuccessResponse<unknown>">json</data-lsp>.<data-lsp lsp="(property) JSONRPC2.ResultResponse<TRPCResult<unknown>>.result: TRPCResult<unknown>">result</data-lsp>.<data-lsp lsp="(property) TRPCResult<unknown>.data: unknown">data</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">  }, []) </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TRouter in <TRouter extends AnyTRPCRouter>(baseUrl: string): DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">TRouter</data-lsp></span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">]&gt;;</span></div><div class="line"><span style="color: #8B949E">//   ^? provide empty array as path to begin with</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>最值得注意的是：路径使用<code>.</code>分隔而非<code>/</code>。这允许我们在服务器端使用单个API处理器处理所有请求（而非为每个过程单独创建）。如果您使用基于文件路由的框架（如Next.js），可能会认出通配路由<code>/api/trpc/[trpc].ts</code>文件——它能匹配所有过程路径。</p>
<p>我们还为<code>fetch</code>请求添加了<code>TRPCResponse</code>类型注解。这定义了服务器响应的JSONRPC兼容格式（详情参阅<a href="https://trpc.io/docs/rpc" target="_blank" rel="noopener noreferrer">rpc文档</a>）。简而言之：响应包含<code>result</code>（结果）或<code>error</code>（错误）对象，据此判断请求是否成功，并在出错时执行相应错误处理。</p>
<p>大功告成！这就是在客户端像调用本地函数一样调用tRPC过程所需的全部代码。表面上看，我们似乎只是通过普通属性访问调用<code>publicProcedure.query / mutation</code>的解析函数，但实际上我们跨越了网络边界——因此可以使用Prisma等服务器端库，同时避免数据库凭证泄漏。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="动手实践">动手实践！<a href="https://trpc.io/zh-Hans/blog/tinyrpc-client#%E5%8A%A8%E6%89%8B%E5%AE%9E%E8%B7%B5" class="hash-link" aria-label="直接跳转到 动手实践！" title="直接跳转到 动手实践！">​</a></h2>
<p>现在创建客户端并配置服务器URL，调用过程时即可享受完整的自动补全和类型安全！</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">url</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'http://localhost:3000/api/trpc'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">createTinyRPCClient</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">AppRouter</span><span style="color: #24292EFF">&gt;(url);</span></div><div class="line"></div><div class="line"><span style="color: #C2C3C5">// 🧙‍♀️ magic autocompletion</span></div><div class="line"></div><div class="line"><span style="color: #1976D2">client</span><span style="color: #24292EFF">.</span><span style="color: #1976D2">post</span><span style="color: #24292EFF">.b;</span></div><div class="line"></div><div class="line"><span style="color: #C2C3C5">// 👀 fully typesafe</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">post</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">post</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">byId</span><span style="color: #6F42C1">.query</span><span style="color: #24292EFF">({ id</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'123'</span><span style="color: #24292EFF"> });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">url</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'http://localhost:3000/api/trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">client</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">createTinyRPCClient</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">AppRouter</span><span style="color: #C9D1D9">&gt;(url);</span></div><div class="line"></div><div class="line"><span style="color: #8B949E">// 🧙‍♀️ magic autocompletion</span></div><div class="line"></div><div class="line"><span style="color: #C9D1D9">client.post.b;</span></div><div class="line"></div><div class="line"><span style="color: #8B949E">// 👀 fully typesafe</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">post</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> client.post.byId.</span><span style="color: #D2A8FF">query</span><span style="color: #C9D1D9">({ id: </span><span style="color: #A5D6FF">'123'</span><span style="color: #C9D1D9"> });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>完整客户端代码可在此处查看<a href="https://github.com/trpc/trpc/blob/main/packages/tests/showcase/tinyrpc.ts" target="_blank" rel="noopener noreferrer">查看</a>，使用示例测试用例参见此处<a href="https://github.com/trpc/trpc/blob/main/packages/tests/showcase/tinyrpc.test.ts" target="_blank" rel="noopener noreferrer">测试用例</a>。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="结语">结语<a href="https://trpc.io/zh-Hans/blog/tinyrpc-client#%E7%BB%93%E8%AF%AD" class="hash-link" aria-label="直接跳转到 结语" title="直接跳转到 结语">​</a></h2>
<p>希望本文能帮助您理解tRPC的工作原理。虽然不建议在生产环境使用此简化版（建议使用仅大几KB的@trpc/client），但官方客户端提供了更多强大功能：</p>
<ul>
<li>
<p>支持查询选项：中止信号、SSR等</p>
</li>
<li>
<p>链接中间件</p>
</li>
<li>
<p>过程批处理</p>
</li>
<li>
<p>WebSocket/订阅功能</p>
</li>
<li>
<p>完善的错误处理</p>
</li>
<li>
<p>数据转换器</p>
</li>
<li>
<p>边缘情况处理（如非tRPC标准响应）</p>
</li>
</ul>
<p>本文未深入探讨服务端实现细节，未来可能会另撰文详述。如有疑问，欢迎通过<a href="https://twitter.com/jullerino" target="_blank" rel="noopener noreferrer">Twitter</a>与我交流。</p>]]></content>
        <author>
            <name>Julius Marminge</name>
            <uri>https://www.jumr.dev/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[v10重构过程中的TypeScript性能优化经验]]></title>
        <id>https://trpc.io/zh-Hans/blog/typescript-performance-lessons</id>
        <link href="https://trpc.io/zh-Hans/blog/typescript-performance-lessons"/>
        <updated>2023-01-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[本页面由 PageTurner AI 翻译（测试版）。未经项目官方认可。]]></summary>
        <content type="html"><![CDATA[<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>非官方测试版翻译</div><div class="admonitionContent_e2NW"><p>本页面由 <a href="https://page-turner.com/" target="_blank" rel="noopener noreferrer"><strong>PageTurner</strong></a> AI 翻译（测试版）。未经项目官方认可。
发现错误？ <a href="https://feedback.page-turner.com/?repo_id=683d130a-1828-4b22-91cd-ef2d269ef3f5&amp;file_path=blog%2F2023-01-14-typescript-performance-lessons.mdx&amp;locale=zh-Hans" target="_blank" rel="noopener noreferrer">报告问题 →</a></p></div></div>
<p>作为库作者，我们的目标是为同行提供最佳的开发者体验（DX）。减少错误排查时间并提供直观的API，能够消除开发者的心智负担，让他们专注于最重要的目标：打造出色的终端用户体验。</p>
<!-- -->
<p>众所周知，TypeScript是tRPC提供卓越开发者体验的核心驱动力。采用TypeScript已成为当今构建优质JavaScript体验的现代标准——但这种类型安全性提升也伴随着某些取舍。</p>
<p>当前TypeScript类型检查器确实存在性能瓶颈（尽管<a href="https://devblogs.microsoft.com/typescript/announcing-typescript-4-9/#performance-improvements" target="_blank" rel="noopener noreferrer">TS 4.9</a>等版本展现出改进潜力！）。库中往往包含最复杂的TypeScript类型魔法，将TS编译器推向极限。正因如此，像我们这样的库作者必须谨慎控制对编译性能的影响，全力确保您的IDE保持流畅运行。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="自动化库性能测试">自动化库性能测试<a href="https://trpc.io/zh-Hans/blog/typescript-performance-lessons#%E8%87%AA%E5%8A%A8%E5%8C%96%E5%BA%93%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95" class="hash-link" aria-label="直接跳转到 自动化库性能测试" title="直接跳转到 自动化库性能测试">​</a></h2>
<p>在tRPC <code>v9</code>阶段，我们开始收到开发者反馈：大型tRPC路由器正对类型检查器产生负面影响。这对tRPC是全新挑战——项目在<code>v9</code>阶段获得巨大采用率，随着开发者构建越来越庞大的tRPC应用，某些性能问题逐渐显现。</p>
<p>您的库_当前_可能并不慢，但随着库的演进，持续关注性能至关重要。通过在每次提交时_以编程方式_测试库代码，自动化测试能显著减轻库开发（及应用构建！）的负担。</p>
<p>在tRPC中，我们通过<a href="https://github.com/trpc/trpc/blob/9fc2d06a8924da73e10b9d4497f3a1f53de706ed/scripts/generateBigBoi.ts" target="_blank" rel="noopener noreferrer">生成</a>并<a href="https://github.com/trpc/trpc/blob/9fc2d06a8924da73e10b9d4497f3a1f53de706ed/packages/tests/server/react/bigBoi.test.tsx" target="_blank" rel="noopener noreferrer">测试</a>包含3,500个过程和1,000个路由器的超级路由来确保稳定性。但这仅测试了TS编译器的崩溃临界点，而非类型检查耗时。我们同时测试库的三个部分（服务端、原生客户端和React客户端），因为它们的代码路径各不相同。过去我们遇到过仅影响库特定模块的回归问题，正是测试帮我们捕捉这些意外行为（我们仍<a href="https://github.com/trpc/trpc/issues/2892" target="_blank" rel="noopener noreferrer">计划深入</a>测量编译时间）。</p>
<p>tRPC并非运行时密集型库，因此性能指标聚焦于类型检查领域。我们始终关注：</p>
<ul>
<li>
<p>使用<code>tsc</code>执行类型检查的速度</p>
</li>
<li>
<p>初始加载时间长短</p>
</li>
<li>
<p>TypeScript语言服务器响应变更的延迟</p>
</li>
</ul>
<p>最后一点是tRPC最需重视的环节。您<strong>绝不</strong>希望开发者在修改代码后等待语言服务器更新。这正是tRPC<strong>必须</strong>坚守性能底线的地方，以确保您享受卓越的DX。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="trpc性能优化机会的发现历程">tRPC性能优化机会的发现历程<a href="https://trpc.io/zh-Hans/blog/typescript-performance-lessons#trpc%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E6%9C%BA%E4%BC%9A%E7%9A%84%E5%8F%91%E7%8E%B0%E5%8E%86%E7%A8%8B" class="hash-link" aria-label="直接跳转到 tRPC性能优化机会的发现历程" title="直接跳转到 tRPC性能优化机会的发现历程">​</a></h2>
<p>TypeScript精确性与编译器性能始终存在权衡。这两者对其他开发者都至关重要，因此我们必须极度谨慎地编写类型：特定类型"过于宽松"是否会导致应用出现严重错误？性能收益是否值得牺牲精确性？</p>
<p>这些优化真能带来显著性能提升吗？问得好。</p>
<p>让我们看看如何在 TypeScript 代码中寻找性能优化机会。我将带您体验创建 <a href="https://github.com/trpc/trpc/pull/2716" target="_blank" rel="noopener noreferrer">PR #2716</a> 的过程，该优化使 TS 编译时间减少了 59%。</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="slug-typescript-performance-lessonstitle-v10重构过程中的typescript性能优化经验authors-sachinraja">slug: typescript-performance-lessons
title: v10重构过程中的TypeScript性能优化经验
authors: [sachinraja]<a href="https://trpc.io/zh-Hans/blog/typescript-performance-lessons#slug-typescript-performance-lessonstitle-v10%E9%87%8D%E6%9E%84%E8%BF%87%E7%A8%8B%E4%B8%AD%E7%9A%84typescript%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E7%BB%8F%E9%AA%8Cauthors-sachinraja" class="hash-link" aria-label="直接跳转到 slug: typescript-performance-lessons
title: v10重构过程中的TypeScript性能优化经验
authors: [sachinraja]" title="直接跳转到 slug: typescript-performance-lessons
title: v10重构过程中的TypeScript性能优化经验
authors: [sachinraja]">​</a></h2>
<p>TypeScript 内置的<a href="https://github.com/microsoft/TypeScript/wiki/Performance-Tracing" target="_blank" rel="noopener noreferrer">性能追踪工具</a>能帮助定位类型系统的瓶颈。虽然不完美，但这是目前最有效的工具。</p>
<p>最佳实践是在真实应用场景中测试您的库。对于 tRPC，我创建了一个基础 <a href="https://create.t3.gg/" target="_blank" rel="noopener noreferrer">T3 应用</a>来模拟用户的实际开发环境。</p>
<p>以下是我追踪 tRPC 性能的步骤：</p>
<ol>
<li>
<p>在示例应用中<a href="https://docs.npmjs.com/cli/commands/npm-link" target="_blank" rel="noopener noreferrer">本地链接库</a>。这样修改库代码后能立即测试效果。</p>
</li>
<li>
<p>在示例应用中运行命令：</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">sh</div><div class="code-container"><code><div class="line"><span style="color: #24292EFF">tsc --generateTrace ./trace --incremental </span><span style="color: #6F42C1">false</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">sh</div><div class="code-container"><code><div class="line"><span style="color: #C9D1D9">tsc --generateTrace ./trace --incremental </span><span style="color: #79C0FF">false</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
</li>
<li>
<p>生成的 <code>trace/trace.json</code> 文件可用 <a href="https://ui.perfetto.dev/" target="_blank" rel="noopener noreferrer">Perfetto</a> 或 <code>chrome://tracing</code> 工具分析。</p>
</li>
</ol>
<p>这里开始变得有趣——我们能观察到应用中类型的性能特征。首次追踪结果如下：
<img decoding="async" loading="lazy" src="https://assets.trpc.io/www/blog/2023-01-14-typescript-performance-lessons/trace-1.png" alt="追踪条显示 src/pages/index.ts 类型检查耗时 332ms" class="img_Njog"></p>
<p>长条表示该过程耗时较长。截图中选中的绿色长条表明 <code>src/pages/index.ts</code> 是瓶颈。<code>Duration</code> 字段显示耗时 332ms——对类型检查而言这是巨大的开销！蓝色 <code>checkVariableDeclaration</code> 条说明编译器大部分时间消耗在某个变量上。
点击该条可查看具体信息：
<img decoding="async" loading="lazy" src="https://assets.trpc.io/www/blog/2023-01-14-typescript-performance-lessons/trace-2.png" alt="追踪信息显示变量位置为 275" class="img_Njog">
<code>pos</code> 字段指出变量在文件中的位置。查看 <code>src/pages/index.ts</code> 对应位置，发现罪魁祸首是 <code>utils = trpc.useContext()</code>！</p>
<p>这怎么可能？我们只是用了个简单的钩子！查看代码：</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> { AppRouter } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'~/server/trpc'</span><span style="color: #24292EFF">;</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">createTRPCReact</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">AppRouter</span><span style="color: #24292EFF">&gt;();</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Home</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">NextPage</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> () </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> { </span><span style="color: #1976D2">data</span><span style="color: #24292EFF"> } </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">r0</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">greeting</span><span style="color: #6F42C1">.useQuery</span><span style="color: #24292EFF">({ who</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'from tRPC'</span><span style="color: #24292EFF"> });</span></div><div class="line"></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">utils</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #6F42C1">.useContext</span><span style="color: #24292EFF">();</span></div><div class="line"></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #1976D2">utils</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">r49</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">greeting</span><span style="color: #6F42C1">.invalidate</span><span style="color: #24292EFF">();</span></div><div class="line"><span style="color: #24292EFF">};</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">default</span><span style="color: #24292EFF"> Home;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> { AppRouter } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'~/server/trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">trpc</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">createTRPCReact</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">AppRouter</span><span style="color: #C9D1D9">&gt;();</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">Home</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">NextPage</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> () </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> { </span><span style="color: #79C0FF">data</span><span style="color: #C9D1D9"> } </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> trpc.r0.greeting.</span><span style="color: #D2A8FF">useQuery</span><span style="color: #C9D1D9">({ who: </span><span style="color: #A5D6FF">'from tRPC'</span><span style="color: #C9D1D9"> });</span></div><div class="line"></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">utils</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> trpc.</span><span style="color: #D2A8FF">useContext</span><span style="color: #C9D1D9">();</span></div><div class="line"></div><div class="line"><span style="color: #C9D1D9">  utils.r49.greeting.</span><span style="color: #D2A8FF">invalidate</span><span style="color: #C9D1D9">();</span></div><div class="line"><span style="color: #C9D1D9">};</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #FFA657"> </span><span style="color: #FF7B72">default</span><span style="color: #FFA657"> </span><span style="color: #C9D1D9">Home;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>表面看这里没什么特别。只有一个 <code>useContext</code> 和查询失效操作，理论上不该产生沉重的类型开销，说明问题藏在更深层。让我们检查其类型定义：</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">DecorateProcedure</span><span style="color: #24292EFF">&lt;</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AnyRouter</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">TProcedure</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Procedure</span><span style="color: #24292EFF">&lt;</span><span style="color: #1976D2">any</span><span style="color: #24292EFF">&gt;</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">TProcedure</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AnyQueryProcedure</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">/**</span></div><div class="line"><span style="color: #C2C3C5">   * </span><span style="color: #D32F2F">@see</span><span style="color: #C2C3C5"> https://tanstack.com/query/v4/docs/framework/react/guides/query-invalidation</span></div><div class="line"><span style="color: #C2C3C5">   */</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">invalidate</span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    input</span><span style="color: #D32F2F">?:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">inferProcedureInput</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TProcedure</span><span style="color: #24292EFF">&gt;</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    filters</span><span style="color: #D32F2F">?:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">InvalidateQueryFilters</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    options</span><span style="color: #D32F2F">?:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">InvalidateOptions</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  )</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Promise</span><span style="color: #24292EFF">&lt;</span><span style="color: #1976D2">void</span><span style="color: #24292EFF">&gt;;</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// ... and so on for all the other React Query utilities</span></div><div class="line"><span style="color: #24292EFF">};</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">DecoratedProcedureUtilsRecord</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AnyRouter</span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">OmitNeverKeys</span><span style="color: #24292EFF">&lt;{</span></div><div class="line"><span style="color: #24292EFF">    [</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">in</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">keyof</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">]]</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">] </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">LegacyV9ProcedureTag</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">never</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">] </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AnyRouter</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">DecoratedProcedureUtilsRecord</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">]&gt;</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">] </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AnyQueryProcedure</span></div><div class="line"><span style="color: #24292EFF">          </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">DecorateProcedure</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TRouter</span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">]&gt;</span></div><div class="line"><span style="color: #24292EFF">          </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">never</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">  }&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">DecorateProcedure</span><span style="color: #C9D1D9">&lt;</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AnyRouter</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">TProcedure</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">Procedure</span><span style="color: #C9D1D9">&lt;</span><span style="color: #79C0FF">any</span><span style="color: #C9D1D9">&gt;,</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">TProcedure</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AnyQueryProcedure</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">/**</span></div><div class="line"><span style="color: #8B949E">   * </span><span style="color: #FF7B72">@see</span><span style="color: #8B949E"> </span><span style="color: #C9D1D9">https://tanstack.com/query/v4/docs/framework/react/guides/query-invalidation</span></div><div class="line"><span style="color: #8B949E">   */</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #D2A8FF">invalidate</span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">input</span><span style="color: #FF7B72">?:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">inferProcedureInput</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TProcedure</span><span style="color: #C9D1D9">&gt;,</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">filters</span><span style="color: #FF7B72">?:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">InvalidateQueryFilters</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">options</span><span style="color: #FF7B72">?:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">InvalidateOptions</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">  )</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">Promise</span><span style="color: #C9D1D9">&lt;</span><span style="color: #79C0FF">void</span><span style="color: #C9D1D9">&gt;;</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// ... and so on for all the other React Query utilities</span></div><div class="line"><span style="color: #C9D1D9">};</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">DecoratedProcedureUtilsRecord</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AnyRouter</span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">OmitNeverKeys</span><span style="color: #C9D1D9">&lt;{</span></div><div class="line"><span style="color: #C9D1D9">    [</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">in</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">keyof</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">]]</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">] </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">LegacyV9ProcedureTag</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">never</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">] </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AnyRouter</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">DecoratedProcedureUtilsRecord</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">]&gt;</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">] </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AnyQueryProcedure</span></div><div class="line"><span style="color: #C9D1D9">          </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">DecorateProcedure</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">, </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">]&gt;</span></div><div class="line"><span style="color: #C9D1D9">          </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">never</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">  }&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>现在需要拆解并理解这些代码的实际行为。</p>
<p>我们有个递归类型 <code>DecoratedProcedureUtilsRecord</code>，它会遍历路由中的所有过程，并给它们"装饰"（添加方法）React Query 的工具方法如 <a href="https://tanstack.com/query/v4/docs/framework/react/guides/query-invalidation" target="_blank" rel="noopener noreferrer"><code>invalidateQueries</code></a>。</p>
<p>在 tRPC v10 中我们仍支持旧版 <code>v9</code> 路由，但 <code>v10</code> 客户端不能调用 <code>v9</code> 路由的过程。因此对每个过程，我们检查它是否是 <code>v9</code> 过程（<code>extends LegacyV9ProcedureTag</code>），如果是则剔除。如果这些操作<strong>没有惰性求值</strong>，对 TypeScript 将是巨大的计算负担。</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="惰性求值">惰性求值<a href="https://trpc.io/zh-Hans/blog/typescript-performance-lessons#%E6%83%B0%E6%80%A7%E6%B1%82%E5%80%BC" class="hash-link" aria-label="直接跳转到 惰性求值" title="直接跳转到 惰性求值">​</a></h3>
<p>这里的问题在于 TypeScript 在类型系统中评估了所有代码，即使它们未被立即使用。我们的代码仅使用了 <code>utils.r49.greeting.invalidate</code>，因此 TypeScript 应该只需展开 <code>r49</code> 属性（路由）→ <code>greeting</code> 属性（操作）→ 最后获取该操作的 <code>invalidate</code> 函数。该代码无需其他类型，而立即解析所有 tRPC 操作对应的 React Query 工具方法类型会不必要地拖慢 TypeScript。TypeScript 会延迟对<strong>对象</strong>属性的类型评估直到它们被直接使用，因此理论上我们的类型应该实现惰性求值...对吗？</p>
<p>实际上它_不完全_是对象。整个结构被 <code>OmitNeverKeys</code> 类型包裹着——这个工具类型会移除对象中值为 <code>never</code> 的键。这正是我们剥离 v9 操作的部分，确保这些属性不会出现在 Intellisense 中。</p>
<p>但这引发了严重的性能问题：我们强制 TypeScript 立即评估<strong>所有</strong>类型的值以检查它们是否为 <code>never</code>。</p>
<p>如何解决？我们需要改造类型系统让它_少做点事_。</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="拥抱惰性求值">拥抱惰性求值<a href="https://trpc.io/zh-Hans/blog/typescript-performance-lessons#%E6%8B%A5%E6%8A%B1%E6%83%B0%E6%80%A7%E6%B1%82%E5%80%BC" class="hash-link" aria-label="直接跳转到 拥抱惰性求值" title="直接跳转到 拥抱惰性求值">​</a></h3>
<p>我们需要让 <code>v10</code> API 更优雅地适配遗留 <code>v9</code> 路由。新的 tRPC 项目不应承受<a href="https://trpc.io/zh-Hans/docs/v10/migrate-from-v9-to-v10#using-interop">互操作模式</a>带来的 TypeScript 性能损耗。</p>
<p>核心思路是重构类型结构。<code>v9</code> 操作与 <code>v10</code> 操作是不同实体，不应在库代码中共享存储空间。在 tRPC 服务端，这意味着我们需要将类型存储到路由的不同字段（而非单一的 <code>record</code> 字段），参考前文的 <code>DecoratedProcedureUtilsRecord</code>。</p>
<p>我们调整了实现：当 <code>v9</code> 路由转换为 <code>v10</code> 路由时，其操作会被注入到 <code>legacy</code> 字段。</p>
<p>旧类型实现：</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V10Router</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TProcedureRecord</span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  record</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TProcedureRecord</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">};</span></div><div class="line"></div><div class="line"><span style="color: #C2C3C5">// convert a v9 interop router to a v10 router</span></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateV9Router</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V9Router</span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V10Router</span><span style="color: #24292EFF">&lt;{</span></div><div class="line"><span style="color: #24292EFF">  [</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">in</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">keyof</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'procedures'</span><span style="color: #24292EFF">]]</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateProcedure</span><span style="color: #24292EFF">&lt;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'procedures'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">]</span></div><div class="line"><span style="color: #24292EFF">  &gt; </span><span style="color: #D32F2F">&amp;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">LegacyV9ProcedureTag</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">}&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V10Router</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TProcedureRecord</span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">record</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TProcedureRecord</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">};</span></div><div class="line"></div><div class="line"><span style="color: #8B949E">// convert a v9 interop router to a v10 router</span></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateV9Router</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V9Router</span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V10Router</span><span style="color: #C9D1D9">&lt;{</span></div><div class="line"><span style="color: #C9D1D9">  [</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">in</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">keyof</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'procedures'</span><span style="color: #C9D1D9">]]</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateProcedure</span><span style="color: #C9D1D9">&lt;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'procedures'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">]</span></div><div class="line"><span style="color: #C9D1D9">  &gt; </span><span style="color: #FF7B72">&amp;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">LegacyV9ProcedureTag</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">}&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>回顾前文 <code>DecoratedProcedureUtilsRecord</code> 类型，我们通过附加 <code>LegacyV9ProcedureTag</code> 在类型层区分 <code>v9</code> 与 <code>v10</code> 操作，并强制阻止 <code>v10</code> 客户端调用 <code>v9</code> 操作。</p>
<p>新类型实现：</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V10Router</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TProcedureRecord</span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  record</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TProcedureRecord</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// by default, no legacy procedures</span></div><div class="line"><span style="color: #24292EFF">  legacy</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {};</span></div><div class="line"><span style="color: #24292EFF">};</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateV9Router</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V9Router</span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// v9 routers inject their procedures into a `legacy` field</span></div><div class="line"><span style="color: #24292EFF">  legacy</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #C2C3C5">// v9 clients require that we filter queries, mutations, subscriptions at the top-level</span></div><div class="line"><span style="color: #24292EFF">    queries</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateProcedureRecord</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'queries'</span><span style="color: #24292EFF">]&gt;;</span></div><div class="line"><span style="color: #24292EFF">    mutations</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateProcedureRecord</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'mutations'</span><span style="color: #24292EFF">]&gt;;</span></div><div class="line"><span style="color: #24292EFF">    subscriptions</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateProcedureRecord</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'subscriptions'</span><span style="color: #24292EFF">]&gt;;</span></div><div class="line"><span style="color: #24292EFF">  };</span></div><div class="line"><span style="color: #24292EFF">} </span><span style="color: #D32F2F">&amp;</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V10Router</span><span style="color: #24292EFF">&lt;</span><span style="color: #C2C3C5">/* empty object, v9 routers have no v10 procedures to pass */</span><span style="color: #24292EFF"> {}&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V10Router</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TProcedureRecord</span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">record</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TProcedureRecord</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// by default, no legacy procedures</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">legacy</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> {};</span></div><div class="line"><span style="color: #C9D1D9">};</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateV9Router</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V9Router</span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// v9 routers inject their procedures into a `legacy` field</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">legacy</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #8B949E">// v9 clients require that we filter queries, mutations, subscriptions at the top-level</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">queries</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateProcedureRecord</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'queries'</span><span style="color: #C9D1D9">]&gt;;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">mutations</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateProcedureRecord</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'mutations'</span><span style="color: #C9D1D9">]&gt;;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">subscriptions</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateProcedureRecord</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'subscriptions'</span><span style="color: #C9D1D9">]&gt;;</span></div><div class="line"><span style="color: #C9D1D9">  };</span></div><div class="line"><span style="color: #C9D1D9">} </span><span style="color: #FF7B72">&amp;</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V10Router</span><span style="color: #C9D1D9">&lt;</span><span style="color: #8B949E">/* empty object, v9 routers have no v10 procedures to pass */</span><span style="color: #C9D1D9"> {}&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>现在我们可以移除 <code>OmitNeverKeys</code>：操作已被预分类，路由的 <code>record</code> 属性类型仅含 <code>v10</code> 操作，而 <code>legacy</code> 属性类型包含所有 <code>v9</code> 操作。我们不再强制 TypeScript 完整评估庞大的 <code>DecoratedProcedureUtilsRecord</code> 类型，同时移除了基于 <code>LegacyV9ProcedureTag</code> 的 <code>v9</code> 操作过滤逻辑。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="成效如何">成效如何？<a href="https://trpc.io/zh-Hans/blog/typescript-performance-lessons#%E6%88%90%E6%95%88%E5%A6%82%E4%BD%95" class="hash-link" aria-label="直接跳转到 成效如何？" title="直接跳转到 成效如何？">​</a></h2>
<p>新版性能追踪显示瓶颈已消除：
<img decoding="async" loading="lazy" src="https://assets.trpc.io/www/blog/2023-01-14-typescript-performance-lessons/trace-3.png" alt="跟踪条显示 src/pages/index.ts 类型检查耗时 136ms" class="img_Njog"></p>
<p>显著提升！类型检查时间从 332ms 降至 136ms 🤯！单看数值可能不明显，但这是重大胜利。200ms 看似短暂，但请思考：</p>
<ul>
<li>
<p>项目中还存在多少其他 TS 库</p>
</li>
<li>
<p>当前有多少开发者使用 tRPC</p>
</li>
<li>
<p>单个工作会话中类型重新评估的次数</p>
</li>
</ul>
<p>无数个 200ms 累加后将形成惊人数字。</p>
<p>我们持续探索优化 TypeScript 开发者体验的方案——无论是在 tRPC 还是其他 TS 项目中。若想探讨 TypeScript，欢迎在 <a href="https://twitter.com/s4chinraja" target="_blank" rel="noopener noreferrer">Twitter</a> @ 我。</p>
<p>感谢 <a href="https://twitter.com/anthonysheww" target="_blank" rel="noopener noreferrer">Anthony Shew</a> 协助撰写本文，并感谢 <a href="https://twitter.com/alexdotjs" target="_blank" rel="noopener noreferrer">Alex</a> 的审阅工作！</p>]]></content>
        <author>
            <name>Sachin Raja</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[宣布 tRPC v10 发布]]></title>
        <id>https://trpc.io/zh-Hans/blog/announcing-trpc-10</id>
        <link href="https://trpc.io/zh-Hans/blog/announcing-trpc-10"/>
        <updated>2022-11-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[本页面由 PageTurner AI 翻译（测试版）。未经项目官方认可。]]></summary>
        <content type="html"><![CDATA[<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>非官方测试版翻译</div><div class="admonitionContent_e2NW"><p>本页面由 <a href="https://page-turner.com/" target="_blank" rel="noopener noreferrer"><strong>PageTurner</strong></a> AI 翻译（测试版）。未经项目官方认可。
发现错误？ <a href="https://feedback.page-turner.com/?repo_id=683d130a-1828-4b22-91cd-ef2d269ef3f5&amp;file_path=blog%2F2022-11-21-announcing-trpc-10.mdx&amp;locale=zh-Hans" target="_blank" rel="noopener noreferrer">报告问题 →</a></p></div></div>
<!-- -->
<p>tRPC 通过 TypeScript 的强大能力强制执行严格的全栈类型绑定，提供卓越的开发者体验。无需担心 API 契约漂移，也无需代码生成。</p>
<!-- -->
<p>自 2021 年 8 月发布上一个主要版本以来，tRPC 社区取得了显著增长：</p>
<ul>
<li>
<p>我们在 GitHub 上已获得超过 <a href="https://github.com/trpc/trpc" target="_blank" rel="noopener noreferrer">15,000 颗星</a></p>
</li>
<li>
<p><a href="https://trpc.io/discord" target="_blank" rel="noopener noreferrer">Discord 社区</a>拥有超过 2,000 名成员</p>
</li>
<li>
<p><a href="https://www.npmjs.com/package/@trpc/server" target="_blank" rel="noopener noreferrer">每周 npm 下载量超过 100k</a></p>
</li>
<li>
<p><a href="https://github.com/trpc/trpc/graphs/contributors" target="_blank" rel="noopener noreferrer">近 200 名贡献者</a></p>
</li>
<li>
<p><a href="https://trpc.io/awesome" target="_blank" rel="noopener noreferrer">不断扩展的扩展、示例和内容生态系统</a></p>
</li>
</ul>
<p><strong>今天，我们正式发布 tRPC v10</strong>。我们很高兴地宣布，v10 已被众多大型 TypeScript 项目投入生产环境使用。本次正式发布标志着 v10 已面向更广泛的社区开放。</p>
<p>对于新项目，您可以通过<a href="https://trpc.io/awesome#-starting-points-example-projects-etc" target="_blank" rel="noopener noreferrer">示例应用</a>快速上手了解 tRPC v10。对于已在享受 tRPC v9 的项目，请<a href="https://trpc.io/docs/v10/migrate-from-v9-to-v10" target="_blank" rel="noopener noreferrer">查阅 v10 迁移指南</a>。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="变更概览">变更概览<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-10#%E5%8F%98%E6%9B%B4%E6%A6%82%E8%A7%88" class="hash-link" aria-label="直接跳转到 变更概览" title="直接跳转到 变更概览">​</a></h2>
<p>v10 是 tRPC 有史以来最重要的版本发布。这是我们首次对 tRPC 的核心架构进行根本性变革，相信这些变化将为开发前沿应用的快速迭代团队开启全新可能。</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="开发者体验升级">开发者体验升级<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-10#%E5%BC%80%E5%8F%91%E8%80%85%E4%BD%93%E9%AA%8C%E5%8D%87%E7%BA%A7" class="hash-link" aria-label="直接跳转到 开发者体验升级" title="直接跳转到 开发者体验升级">​</a></h3>
<p>tRPC v10 深度拥抱您的 IDE。我们不仅统一了类型系统，更在本版本中无缝整合了您的前端、后端和编码体验。</p>
<p>在 v10 中，您可以：</p>
<ul>
<li>
<p>使用 <em>"转到定义"</em> 功能从前端调用点直接跳转至后端过程实现</p>
</li>
<li>
<p>使用 <em>"重命名符号"</em> 为输入参数或过程在整个应用中赋予新名称</p>
</li>
<li>
<p>更便捷地<a href="https://trpc.io/docs/v10/infer-types" target="_blank" rel="noopener noreferrer">推断类型</a>，在应用中手动使用 tRPC 类型</p>
</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="强大的后端框架">强大的后端框架<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-10#%E5%BC%BA%E5%A4%A7%E7%9A%84%E5%90%8E%E7%AB%AF%E6%A1%86%E6%9E%B6" class="hash-link" aria-label="直接跳转到 强大的后端框架" title="直接跳转到 强大的后端框架">​</a></h3>
<p>在 v10 中，我们重构了后端过程定义语法，为您以更合理的方式实现业务逻辑提供了更大空间。本版本特性包括：</p>
<ul>
<li>
<p>支持<a href="https://trpc.io/docs/v10/middlewares#context-extension" target="_blank" rel="noopener noreferrer">上下文扩展</a>的<a href="https://trpc.io/docs/v10/middlewares" target="_blank" rel="noopener noreferrer">可复用中间件</a></p>
</li>
<li>
<p>支持<a href="https://trpc.io/docs/v10/procedures#multiple-input-parsers" target="_blank" rel="noopener noreferrer">多输入解析器</a>的<a href="https://trpc.io/docs/v10/procedures#reusable-base-publicprocedures" target="_blank" rel="noopener noreferrer">可链式复用过程</a></p>
</li>
<li>
<p>支持<a href="https://trpc.io/docs/v10/error-formatting" target="_blank" rel="noopener noreferrer">自定义错误格式化</a>的灵活<a href="https://trpc.io/docs/v10/error-handling" target="_blank" rel="noopener noreferrer">错误处理</a></p>
</li>
<li>
<p>通过<a href="https://trpc.io/docs/v10/metadata" target="_blank" rel="noopener noreferrer">过程元数据</a>为过程添加扩展信息</p>
</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="typescript-性能大幅提升">TypeScript 性能大幅提升<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-10#typescript-%E6%80%A7%E8%83%BD%E5%A4%A7%E5%B9%85%E6%8F%90%E5%8D%87" class="hash-link" aria-label="直接跳转到 TypeScript 性能大幅提升" title="直接跳转到 TypeScript 性能大幅提升">​</a></h3>
<p>TypeScript 使开发者能够实现非凡功能，但这可能带来性能代价。我们用于保持类型严格性的诸多技术会给 TypeScript 编译器带来沉重负担。社区反馈表明，大型 tRPC v9 应用已因此出现 IDE 性能下降问题。</p>
<p>我们的目标是提升各种规模应用的开发体验。在 v10 版本中，我们显著改善了 TypeScript 性能（特别是配合 TS 增量编译），确保您的编辑器始终保持流畅响应。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="渐进式迁移">渐进式迁移<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-10#%E6%B8%90%E8%BF%9B%E5%BC%8F%E8%BF%81%E7%A7%BB" class="hash-link" aria-label="直接跳转到 渐进式迁移" title="直接跳转到 渐进式迁移">​</a></h2>
<p>我们还投入大量精力使迁移体验尽可能顺畅，包括提供可实现（几乎）完全向后兼容 v9 路由器的 <code>interop()</code> 方法。更多细节请查阅<a href="https://trpc.io/docs/v10/migrate-from-v9-to-v10" target="_blank" rel="noopener noreferrer">迁移指南</a>。</p>
<p>核心团队成员 <a href="https://twitter.com/s4chinraja" target="_blank" rel="noopener noreferrer">Sachin</a> 还开发了<a href="https://github.com/sachinraja/trpc-v10-migrate-codemod" target="_blank" rel="noopener noreferrer">自动化代码迁移工具</a>，可为您完成大部分繁重的迁移工作。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="蓬勃发展的生态">蓬勃发展的生态<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-10#%E8%93%AC%E5%8B%83%E5%8F%91%E5%B1%95%E7%9A%84%E7%94%9F%E6%80%81" class="hash-link" aria-label="直接跳转到 蓬勃发展的生态" title="直接跳转到 蓬勃发展的生态">​</a></h2>
<p>围绕 tRPC 正在形成丰富的子工具库生态，典型案例如下：</p>
<ul>
<li>
<p><a href="https://github.com/jlalmes/trpc-openapi" target="_blank" rel="noopener noreferrer">trpc-openapi</a> 轻松创建兼容 REST 规范的端点</p>
</li>
<li>
<p><a href="https://github.com/t3-oss/create-t3-app" target="_blank" rel="noopener noreferrer">create-t3-app</a> 快速搭建包含 tRPC 的全栈 Next.js 应用</p>
</li>
<li>
<p><a href="https://github.com/t3-oss/create-t3-turbo" target="_blank" rel="noopener noreferrer">create-t3-turbo</a> 为 React Native 应用集成 tRPC 的启动模板</p>
</li>
<li>
<p><a href="https://github.com/jlalmes/trpc-chrome" target="_blank" rel="noopener noreferrer">trpc-chrome</a> 构建基于 tRPC 的 Chrome 扩展</p>
</li>
<li>
<p>适配 <a href="https://github.com/OrJDev/solid-trpc" target="_blank" rel="noopener noreferrer">Solid</a>、<a href="https://github.com/icflorescu/trpc-sveltekit-example" target="_blank" rel="noopener noreferrer">Svelte</a>、<a href="https://github.com/michealroberts/usetrpc" target="_blank" rel="noopener noreferrer">Vue</a> 等框架的解决方案</p>
</li>
</ul>
<p>更多插件、示例和适配器请访问 <a href="https://trpc.io/awesome" target="_blank" rel="noopener noreferrer">Awesome tRPC 资源集</a>。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="致谢">致谢！<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-10#%E8%87%B4%E8%B0%A2" class="hash-link" aria-label="直接跳转到 致谢！" title="直接跳转到 致谢！">​</a></h2>
<p>核心团队在此郑重宣告：这仅仅是个开始。我们已着手探索 <a href="https://github.com/reactjs/rfcs/pull/229" target="_blank" rel="noopener noreferrer">React Server Components</a> 与 Next.js 13 的创新结合。</p>
<p>特别向 <a href="https://twitter.com/s4chinraja" target="_blank" rel="noopener noreferrer">Sachin</a>、<a href="https://twitter.com/jullerino" target="_blank" rel="noopener noreferrer">Julius</a>、<a href="https://twitter.com/jlalmes" target="_blank" rel="noopener noreferrer">James</a>、<a href="https://twitter.com/ixahmedxii" target="_blank" rel="noopener noreferrer">Ahmed</a>、<a href="https://twitter.com/trashh_dev" target="_blank" rel="noopener noreferrer">Chris</a>、<a href="https://twitter.com/theo" target="_blank" rel="noopener noreferrer">Theo</a>、<a href="https://twitter.com/shewDev" target="_blank" rel="noopener noreferrer">Anthony</a> 以及<a href="https://github.com/trpc/trpc#all-contributors" target="_blank" rel="noopener noreferrer">所有贡献者</a>致以崇高敬意——没有你们就没有这个版本。</p>
<p>感谢您对 tRPC 的支持与厚爱。</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="slug-typescript-performance-lessonstitle-v10重构过程中的typescript性能优化经验authors-sachinraja">slug: typescript-performance-lessons
title: v10重构过程中的TypeScript性能优化经验
authors: [sachinraja]<a href="https://trpc.io/zh-Hans/blog/announcing-trpc-10#slug-typescript-performance-lessonstitle-v10%E9%87%8D%E6%9E%84%E8%BF%87%E7%A8%8B%E4%B8%AD%E7%9A%84typescript%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E7%BB%8F%E9%AA%8Cauthors-sachinraja" class="hash-link" aria-label="直接跳转到 slug: typescript-performance-lessons
title: v10重构过程中的TypeScript性能优化经验
authors: [sachinraja]" title="直接跳转到 slug: typescript-performance-lessons
title: v10重构过程中的TypeScript性能优化经验
authors: [sachinraja]">​</a></h2>
<ul>
<li>
<p>在 Twitter 关注 <a href="https://twitter.com/trpcio" target="_blank" rel="noopener noreferrer">@trpcio</a></p>
</li>
<li>
<p>加入我们的 <a href="https://trpc.io/discord" target="_blank" rel="noopener noreferrer">Discord 社区</a></p>
</li>
<li>
<p><a href="https://trpc.io/#try-it-out" target="_blank" rel="noopener noreferrer">在浏览器中体验 tRPC</a></p>
</li>
</ul>
<a id="sponsor-button" href="https://trpc.io/sponsor" class="group flex h-12 w-max items-center gap-4 rounded-lg border-2 bg-zinc-200 px-4 py-2 transition hover:bg-zinc-100 dark:border-zinc-900 dark:bg-zinc-800 hover:dark:border-zinc-700 hover:dark:bg-zinc-900"><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="aspect-square h-6 fill-pink-500 transition-transform duration-200 ease-in group-hover:scale-110"><path d="M17.625 1.499c-2.32 0-4.354 1.203-5.625 3.03-1.271-1.827-3.305-3.03-5.625-3.03C3.129 1.499 0 4.253 0 8.249c0 4.275 3.068 7.847 5.828 10.227a33.14 33.14 0 0 0 5.616 3.876l.028.017.008.003-.001.003c.163.085.342.126.521.125.179.001.358-.041.521-.125l-.001-.003.008-.003.028-.017a33.14 33.14 0 0 0 5.616-3.876C20.932 16.096 24 12.524 24 8.249c0-3.996-3.129-6.75-6.375-6.75zm-.919 15.275a30.766 30.766 0 0 1-4.703 3.316l-.004-.002-.004.002a30.955 30.955 0 0 1-4.703-3.316c-2.677-2.307-5.047-5.298-5.047-8.523 0-2.754 2.121-4.5 4.125-4.5 2.06 0 3.914 1.479 4.544 3.684.143.495.596.797 1.086.796.49.001.943-.302 1.085-.796.63-2.205 2.484-3.684 4.544-3.684 2.004 0 4.125 1.746 4.125 4.5 0 3.225-2.37 6.216-5.048 8.523z"></path></svg><span class="font-semibold text-zinc-900 no-underline dark:text-zinc-300">成为赞助商！</span></a>]]></content>
        <author>
            <name>Alex / KATT 🐱</name>
            <uri>https://x.com/alexdotjs</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[认识 tRPC]]></title>
        <id>https://trpc.io/zh-Hans/blog/introducing-trpc</id>
        <link href="https://trpc.io/zh-Hans/blog/introducing-trpc"/>
        <updated>2021-05-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[本页面由 PageTurner AI 翻译（测试版）。未经项目官方认可。]]></summary>
        <content type="html"><![CDATA[<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>非官方测试版翻译</div><div class="admonitionContent_e2NW"><p>本页面由 <a href="https://page-turner.com/" target="_blank" rel="noopener noreferrer"><strong>PageTurner</strong></a> AI 翻译（测试版）。未经项目官方认可。
发现错误？ <a href="https://feedback.page-turner.com/?repo_id=683d130a-1828-4b22-91cd-ef2d269ef3f5&amp;file_path=blog%2F2021-05-05-hello-world.mdx&amp;locale=zh-Hans" target="_blank" rel="noopener noreferrer">报告问题 →</a></p></div></div>
<p>tRPC 让你在（Node）服务器和客户端之间实现端到端的类型安全，<em>甚至无需声明类型</em>。在服务端只需返回函数数据，前端即可根据端点名称直接使用对应数据。</p>
<!-- -->
<p>👋 我是 Alex，GitHub 上的"KATT"，今天要介绍一个叫 <a href="https://trpc.io/" target="_blank" rel="noopener noreferrer">tRPC</a> 的库。虽然还没发表过相关文章（但 GitHub 已神奇收获 &gt;530 🌟），这篇入门指南算是抛砖引玉。后续将有更多文章和视频教程！想获取更新或提问，欢迎在 Twitter 关注 <a href="https://twitter.com/alexdotjs" target="_blank" rel="noopener noreferrer">@alexdotjs</a>。</p>
<p><strong>这是调用 tRPC 端点与客户端的实际效果：</strong>
<img decoding="async" loading="lazy" src="https://assets.trpc.io/www/v9/trpcgif.gif" alt="示例动图" class="img_Njog"></p>
<p>我为 React 开发了专属库 (<code>@trpc/react</code>)，基于优秀的 react-query 实现。而客户端库 (<code>@trpc/client</code>) 可独立于 React 运行（如需开发 Svelte/Vue/Angular/[其他]版本库，欢迎联系我！）</p>
<p>整个过程无需代码生成，你可以轻松集成到现有 Next.js/CRA/Express 项目中。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="示例">示例<a href="https://trpc.io/zh-Hans/blog/introducing-trpc#%E7%A4%BA%E4%BE%8B" class="hash-link" aria-label="直接跳转到 示例" title="直接跳转到 示例">​</a></h2>
<p>这是一个名为 <code>hello</code> 的 tRPC 过程（即端点），接收 <code>string</code> 类型参数：</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">appRouter</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #6F42C1">.router</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.query</span><span style="color: #24292EFF">(</span><span style="color: #22863A">'hello'</span><span style="color: #212121">,</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  input</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.string</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.optional</span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">resolve</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> ({ input }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      text</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">`hello </span><span style="color: #D32F2F">${</span><span style="color: #24292EFF">input </span><span style="color: #D32F2F">??</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'world'</span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    };</span></div><div class="line"><span style="color: #24292EFF">  }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AppRouter</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">typeof</span><span style="color: #24292EFF"> appRouter;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">appRouter</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> trpc.</span><span style="color: #D2A8FF">router</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">query</span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'hello'</span><span style="color: #C9D1D9">, {</span></div><div class="line"><span style="color: #C9D1D9">  input: z.</span><span style="color: #D2A8FF">string</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">optional</span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #D2A8FF">resolve</span><span style="color: #C9D1D9">: ({ </span><span style="color: #FFA657">input</span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">      text: </span><span style="color: #A5D6FF">`hello ${</span><span style="color: #C9D1D9">input</span><span style="color: #A5D6FF"> </span><span style="color: #FF7B72">??</span><span style="color: #A5D6FF"> </span><span style="color: #A5D6FF">'world'}`</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    };</span></div><div class="line"><span style="color: #C9D1D9">  },</span></div><div class="line"><span style="color: #C9D1D9">});</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AppRouter</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">typeof</span><span style="color: #C9D1D9"> appRouter;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>以下是使用该数据的类型安全客户端：</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> { AppRouter } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./server'</span><span style="color: #24292EFF">;</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">function</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">main</span><span style="color: #24292EFF">() {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">createTRPCClient</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">AppRouter</span><span style="color: #24292EFF">&gt;({</span></div><div class="line"><span style="color: #24292EFF">    url</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">`http://localhost:2022`</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  });</span></div><div class="line"></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">result</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #6F42C1">.query</span><span style="color: #24292EFF">(</span><span style="color: #22863A">'hello'</span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@alexdotjs'</span><span style="color: #24292EFF">);</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #1976D2">console</span><span style="color: #6F42C1">.log</span><span style="color: #24292EFF">(result); </span><span style="color: #C2C3C5">// --&gt; { text: "hello @alexdotjs" }</span></div><div class="line"><span style="color: #24292EFF">}</span></div><div class="line"></div><div class="line"><span style="color: #6F42C1">main</span><span style="color: #24292EFF">();</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> { AppRouter } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./server'</span><span style="color: #C9D1D9">;</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">function</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">main</span><span style="color: #C9D1D9">() {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">client</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">createTRPCClient</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">AppRouter</span><span style="color: #C9D1D9">&gt;({</span></div><div class="line"><span style="color: #C9D1D9">    url: </span><span style="color: #A5D6FF">`http://localhost:2022`</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div><div class="line"></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">result</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> client.</span><span style="color: #D2A8FF">query</span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'hello'</span><span style="color: #C9D1D9">, </span><span style="color: #A5D6FF">'@alexdotjs'</span><span style="color: #C9D1D9">);</span></div><div class="line"><span style="color: #C9D1D9">  console.</span><span style="color: #D2A8FF">log</span><span style="color: #C9D1D9">(result); </span><span style="color: #8B949E">// --&gt; { text: "hello @alexdotjs" }</span></div><div class="line"><span style="color: #C9D1D9">}</span></div><div class="line"></div><div class="line"><span style="color: #D2A8FF">main</span><span style="color: #C9D1D9">();</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p><strong>这就是实现类型安全的全部所需！</strong> <code>result</code> 的类型由后端函数返回内容自动推断。输入数据也会通过验证器进行类型推断，因此数据可直接安全使用——实际上，输入数据_必须_通过验证器处理（tRPC 默认支持 zod/yup/自定义验证器）。</p>
<p>这是上面示例的 CodeSandbox 链接：<a href="https://githubbox.com/trpc/trpc/tree/main/examples/standalone-server" target="_blank" rel="noopener noreferrer">https://githubbox.com/trpc/trpc/tree/main/examples/standalone-server</a> （请查看终端输出而非页面预览！）</p>
<p><strong><em>等等？我这是在把后端代码导入前端？</em>——其实不然</strong></p>
<p>虽然看起来像共享代码，但服务器到客户端实际没有传输任何代码逻辑；TypeScript 的 <code>import type</code> "[...] 仅导入用于类型注解的声明，它会被完全擦除，运行时不留痕迹。"——这是 TypeScript 3.8 的特性，详见<a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#:~:text=import%20type%20only%20imports%20declarations,also%20erased%20from%20TypeScript's%20output." target="_blank" rel="noopener noreferrer">官方文档</a>。</p>
<p>整个过程无需代码生成，只要能在客户端共享服务端类型（推荐使用 monorepo），现在就能集成到你的应用中。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="但这只是开始">但这只是开始！<a href="https://trpc.io/zh-Hans/blog/introducing-trpc#%E4%BD%86%E8%BF%99%E5%8F%AA%E6%98%AF%E5%BC%80%E5%A7%8B" class="hash-link" aria-label="直接跳转到 但这只是开始！" title="直接跳转到 但这只是开始！">​</a></h2>
<p>前文提到的 React 库，在组件中这样使用数据：</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> { </span><span style="color: #1976D2">data</span><span style="color: #24292EFF"> } </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #6F42C1">.useQuery</span><span style="color: #24292EFF">([</span><span style="color: #22863A">'hello'</span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@alexdotjs'</span><span style="color: #24292EFF">]);</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> { </span><span style="color: #79C0FF">data</span><span style="color: #C9D1D9"> } </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> trpc.</span><span style="color: #D2A8FF">useQuery</span><span style="color: #C9D1D9">([</span><span style="color: #A5D6FF">'hello'</span><span style="color: #C9D1D9">, </span><span style="color: #A5D6FF">'@alexdotjs'</span><span style="color: #C9D1D9">]);</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>即可在客户端获得类型安全的数据。</p>
<p>现有项目（支持 Express/Next.js 适配器）可立即集成 tRPC，兼容 CRA 且支持 React Native。它甚至不依赖 React，如需开发 Svelte 或 Vue 版本库，请联系我。</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="数据变更如何处理">数据变更如何处理？<a href="https://trpc.io/zh-Hans/blog/introducing-trpc#%E6%95%B0%E6%8D%AE%E5%8F%98%E6%9B%B4%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86" class="hash-link" aria-label="直接跳转到 数据变更如何处理？" title="直接跳转到 数据变更如何处理？">​</a></h2>
<p>变更操作(mutation)与查询同样简单，底层实现相同，仅作为语法糖区别暴露：变更发起 HTTP POST 请求而非 GET 请求。</p>
<p>这里有个稍复杂的数据库使用示例，取自我们的 TodoMVC 示例 todomvc.trpc.io / <a href="https://github.com/trpc/trpc/tree/main/examples/next-prisma-todomvc" target="_blank" rel="noopener noreferrer">https://github.com/trpc/trpc/tree/main/examples/next-prisma-todomvc</a></p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">todoRouter</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">createRouter</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.mutation</span><span style="color: #24292EFF">(</span><span style="color: #22863A">'add'</span><span style="color: #212121">,</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  input</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.object</span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    id</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.string</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.uuid</span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    data</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.object</span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      completed</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.boolean</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.optional</span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      text</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.string</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.min</span><span style="color: #24292EFF">(</span><span style="color: #1976D2">1</span><span style="color: #24292EFF">)</span><span style="color: #6F42C1">.optional</span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">resolve</span><span style="color: #24292EFF">({ ctx</span><span style="color: #212121">,</span><span style="color: #24292EFF"> input }) {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> { </span><span style="color: #1976D2">id</span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">data</span><span style="color: #24292EFF"> } </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> input;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">todo</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">ctx</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">task</span><span style="color: #6F42C1">.update</span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      where</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> { id }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      data</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    });</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> todo;</span></div><div class="line"><span style="color: #24292EFF">  }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">todoRouter</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">createRouter</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">mutation</span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'add'</span><span style="color: #C9D1D9">, {</span></div><div class="line"><span style="color: #C9D1D9">  input: z.</span><span style="color: #D2A8FF">object</span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    id: z.</span><span style="color: #D2A8FF">string</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">uuid</span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">    data: z.</span><span style="color: #D2A8FF">object</span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      completed: z.</span><span style="color: #D2A8FF">boolean</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">optional</span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">      text: z.</span><span style="color: #D2A8FF">string</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">min</span><span style="color: #C9D1D9">(</span><span style="color: #79C0FF">1</span><span style="color: #C9D1D9">).</span><span style="color: #D2A8FF">optional</span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  }),</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">resolve</span><span style="color: #C9D1D9">({ </span><span style="color: #FFA657">ctx</span><span style="color: #C9D1D9">, </span><span style="color: #FFA657">input</span><span style="color: #C9D1D9"> }) {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> { </span><span style="color: #79C0FF">id</span><span style="color: #C9D1D9">, </span><span style="color: #79C0FF">data</span><span style="color: #C9D1D9"> } </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> input;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">todo</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> ctx.task.</span><span style="color: #D2A8FF">update</span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      where: { id },</span></div><div class="line"><span style="color: #C9D1D9">      data,</span></div><div class="line"><span style="color: #C9D1D9">    });</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> todo;</span></div><div class="line"><span style="color: #C9D1D9">  },</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>其 <strong>React 用法</strong> 如下所示：</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">addTask</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #6F42C1">.useMutation</span><span style="color: #24292EFF">(</span><span style="color: #22863A">'todos.add'</span><span style="color: #24292EFF">);</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> (</span></div><div class="line"><span style="color: #24292EFF">  &lt;&gt;</span></div><div class="line"><span style="color: #24292EFF">    &lt;</span><span style="color: #22863A">input</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">placeholder</span><span style="color: #D32F2F">=</span><span style="color: #22863A">"What needs to be done?"</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">onKeyDown</span><span style="color: #D32F2F">=</span><span style="color: #24292EFF">{(e) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">text</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">e</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">currentTarget</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">value</span><span style="color: #6F42C1">.trim</span><span style="color: #24292EFF">();</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #1976D2">e</span><span style="color: #24292EFF">.key </span><span style="color: #D32F2F">===</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'Enter'</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">&amp;&amp;</span><span style="color: #24292EFF"> text) {</span></div><div class="line"><span style="color: #24292EFF">          </span><span style="color: #1976D2">addTask</span><span style="color: #6F42C1">.mutate</span><span style="color: #24292EFF">({ text });</span></div><div class="line"><span style="color: #24292EFF">          </span><span style="color: #1976D2">e</span><span style="color: #24292EFF">.</span><span style="color: #1976D2">currentTarget</span><span style="color: #24292EFF">.value </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">''</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">        }</span></div><div class="line"><span style="color: #24292EFF">      }}</span></div><div class="line"><span style="color: #24292EFF">    /&gt;</span></div><div class="line"><span style="color: #24292EFF">  &lt;/&gt;</span></div><div class="line"><span style="color: #24292EFF">)</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">addTask</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> trpc.</span><span style="color: #D2A8FF">useMutation</span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'todos.add'</span><span style="color: #C9D1D9">);</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> (</span></div><div class="line"><span style="color: #C9D1D9">  &lt;&gt;</span></div><div class="line"><span style="color: #C9D1D9">    &lt;</span><span style="color: #7EE787">input</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #79C0FF">placeholder</span><span style="color: #FF7B72">=</span><span style="color: #A5D6FF">"What needs to be done?"</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #79C0FF">onKeyDown</span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9">{(</span><span style="color: #FFA657">e</span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">text</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> e.currentTarget.value.</span><span style="color: #D2A8FF">trim</span><span style="color: #C9D1D9">();</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (e.key </span><span style="color: #FF7B72">===</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'Enter'</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">&amp;&amp;</span><span style="color: #C9D1D9"> text) {</span></div><div class="line"><span style="color: #C9D1D9">          addTask.</span><span style="color: #D2A8FF">mutate</span><span style="color: #C9D1D9">({ text });</span></div><div class="line"><span style="color: #C9D1D9">          e.currentTarget.value </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">''</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">        }</span></div><div class="line"><span style="color: #C9D1D9">      }}</span></div><div class="line"><span style="color: #C9D1D9">    /&gt;</span></div><div class="line"><span style="color: #C9D1D9">  &lt;/&gt;</span></div><div class="line"><span style="color: #C9D1D9">)</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="暂时到此为止">暂时到此为止<a href="https://trpc.io/zh-Hans/blog/introducing-trpc#%E6%9A%82%E6%97%B6%E5%88%B0%E6%AD%A4%E4%B8%BA%E6%AD%A2" class="hash-link" aria-label="直接跳转到 暂时到此为止" title="直接跳转到 暂时到此为止">​</a></h2>
<p>总之，正如我所说，我只是想开个头。还有很多功能值得探索：</p>
<ul>
<li>
<p>为请求创建上下文（用于注入用户数据到解析器）- <a href="https://trpc.io/zh-Hans/docs/v9/context">链接</a></p>
</li>
<li>
<p>路由中间件支持 - <a href="https://trpc.io/zh-Hans/docs/v9/middlewares">链接</a></p>
</li>
<li>
<p>路由合并（你可能不希望所有后端逻辑都在单个文件中）- <a href="https://trpc.io/zh-Hans/docs/v9/merging-routers">链接</a></p>
</li>
<li>
<p>通过 <code>@trpc/next</code> 适配器实现极简服务端渲染 - <a href="https://trpc.io/zh-Hans/docs/v9/">链接</a></p>
</li>
<li>
<p>类型安全的错误格式化 - <a href="https://trpc.io/zh-Hans/docs/v9/error-formatting">链接</a></p>
</li>
<li>
<p>数据转换器（支持 Date/Map/Set 跨网络传输）- <a href="https://trpc.io/zh-Hans/docs/v9/data-transformers">链接</a></p>
</li>
<li>
<p>React Query 辅助工具</p>
</li>
</ul>
<p>如需快速入门，可参考 <a href="https://trpc.io/zh-Hans/docs/v9/nextjs">Next.js 入门指南</a> 中的示例项目。</p>
<p><a href="https://twitter.com/alexdotjs" target="_blank" rel="noopener noreferrer">在 Twitter 关注我获取最新动态！</a></p>]]></content>
        <author>
            <name>Alex / KATT 🐱</name>
            <uri>https://x.com/alexdotjs</uri>
        </author>
    </entry>
</feed>