{"id":8524,"date":"2026-02-13T12:00:13","date_gmt":"2026-02-13T17:00:13","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=8524"},"modified":"2026-02-13T12:01:11","modified_gmt":"2026-02-13T17:01:11","slug":"fun-with-typescript-generics","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/fun-with-typescript-generics\/","title":{"rendered":"Fun with TypeScript Generics"},"content":{"rendered":"\n<p>Generics are an incredibly powerful feature of <a href=\"https:\/\/www.typescriptlang.org\/\">TypeScript<\/a>. There&#8217;s endless content on TypeScript in general, and generics in particular. This post will differ a bit and cover things more deeply.<\/p>\n\n\n\n<p>This won&#8217;t be a generic introduction to generics (pun intended). Instead, we&#8217;ll implement a very, very niche use case, and in the process cover some advanced uses for generics, plus conditional types, and some other goodies.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"a-quick-refresher-on-generics-and-conditional-types\">A Quick Refresher on Generics &amp; Conditional Types<\/h2>\n\n\n\n<p>Let&#8217;s take a very, very fast introduction to the key concepts of this post. We&#8217;ll use extremely contrived examples to keep everything as brief as possible.<\/p>\n\n\n\n<p>If you&#8217;re already an expert, just scroll past. If you&#8217;re not sure, give it a read, and if what&#8217;s in this section isn&#8217;t old hat, you might want to read some refresher materials before tackling the rest of this post.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"generics\">Generics<\/h3>\n\n\n\n<p>Think of generics as function parameters that are types. What do I mean by that? Normally function parameters are <em>values<\/em> (or references to a value, but we won&#8217;t bother with that).<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">arrayLength<\/span>(<span class=\"hljs-params\">arr: <span class=\"hljs-built_in\">any<\/span>&#91;]<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> arr.length;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Here, <code>arr<\/code> is an array. Right now, it&#8217;s an array of <code>any<\/code>. If we wanted, we could type this array a bit more accurately by adding a generic argument.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">arrayLengthTyped<\/span>&lt;<span class=\"hljs-title\">T<\/span>&gt;(<span class=\"hljs-params\">arr: T&#91;]<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> arr.length;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now, whenever we call this method and pass an array, the generic argument <code>T<\/code> will infer to whatever the type of the array is. Make no mistake, even though <code>T<\/code> makes this method definition more accurate, it&#8217;s completely pointless. The original method was perfectly fine. The value of <code>arr<\/code> is an array of <code>any<\/code>, but it doesn&#8217;t matter; no matter what the elements of the array are, the <code>.length<\/code> property will always be there.<\/p>\n\n\n\n<p>Let\u2019s go from one pointless function to another. Let\u2019s implement our own filter.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">filterUntyped<\/span>(<span class=\"hljs-params\">array: <span class=\"hljs-built_in\">any<\/span>&#91;], predicate: (item: <span class=\"hljs-built_in\">any<\/span>) =&gt; <span class=\"hljs-built_in\">boolean<\/span><\/span>): <span class=\"hljs-title\">any<\/span>&#91;] <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> array.filter(predicate);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This time we actually have a problem. There&#8217;s absolutely no checking done on the predicate function we pass in.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">type<\/span> User = {\n  name: <span class=\"hljs-built_in\">string<\/span>;\n};\n\n<span class=\"hljs-keyword\">const<\/span> users: User&#91;] = &#91;];\n\nfilterUntyped(users, <span class=\"hljs-function\"><span class=\"hljs-params\">user<\/span> =&gt;<\/span> user.nameX === <span class=\"hljs-string\">\"John\"<\/span>);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We\u2019re passing in a function that takes each member of the array, but we\u2019re clearly misusing it; there is no nameX property on each user. This is where generics shine.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">filterTyped<\/span>&lt;<span class=\"hljs-title\">T<\/span>&gt;(<span class=\"hljs-params\">array: T&#91;], predicate: (item: T) =&gt; <span class=\"hljs-built_in\">boolean<\/span><\/span>): <span class=\"hljs-title\">T<\/span>&#91;] <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> array.filter(predicate);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now TypeScript will verify everything.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">filterTyped(users, user =&gt; user.nameX === \"John\");<br><em>\/\/ -----------------------------^^^^^<\/em><br><em>\/\/ Property 'nameX' does not exist on type 'User'. Did you mean 'name'?<\/em><\/pre>\n\n\n\n<p>We can even limit generic arguments. What if we have a bunch of different user types?<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">type<\/span> User = {\n  name: <span class=\"hljs-built_in\">string<\/span>;\n};\n\n<span class=\"hljs-keyword\">type<\/span> AdminUser = User &amp; {\n  role: <span class=\"hljs-built_in\">string<\/span>;\n};\n\n<span class=\"hljs-keyword\">type<\/span> BannedUser = User &amp; {\n  reason: <span class=\"hljs-built_in\">string<\/span>;\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>For whatever strange reason, we wanted to take the <code>filterTyped<\/code> function from before.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">filterTyped<\/span>&lt;<span class=\"hljs-title\">T<\/span>&gt;(<span class=\"hljs-params\">array: T&#91;], predicate: (item: T) =&gt; <span class=\"hljs-built_in\">boolean<\/span><\/span>): <span class=\"hljs-title\">T<\/span>&#91;] <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> array.filter(predicate);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>But this time have it only works with any <code>User<\/code> type.<\/p>\n\n\n\n<p>If you&#8217;re thinking <em>just ditch the generics altogether and&#8230;<\/em><\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">filterUser<\/span>(<span class=\"hljs-params\">array: User&#91;], predicate: (item: User) =&gt; <span class=\"hljs-built_in\">boolean<\/span><\/span>): <span class=\"hljs-title\">User<\/span>&#91;] <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> array.filter(predicate);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>&#8230;not so fast. This function, while appealing, winds up erasing our return type.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> adminUsers: AdminUser&#91;] = &#91;];\n<span class=\"hljs-keyword\">const<\/span> adminUsersNamedAdam = filterUser(adminUsers, <span class=\"hljs-function\"><span class=\"hljs-params\">user<\/span> =&gt;<\/span> user.name === <span class=\"hljs-string\">\"Adam\"<\/span>);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The variable&nbsp;<code>adminUsersNamedAdam<\/code>&nbsp;is typed as&nbsp;<code>User[]<\/code>, and how could it not be?&nbsp;<code>filterUser<\/code>&nbsp;is explicitly typed to return&nbsp;<code>User[].<\/code><\/p>\n\n\n\n<p>The correct solution is to go back to the generic version, but <em>restrict<\/em> the acceptable values for T.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">filterUserCorrect<\/span>&lt;<span class=\"hljs-title\">T<\/span> <span class=\"hljs-title\">extends<\/span> <span class=\"hljs-title\">User<\/span>&gt;(<span class=\"hljs-params\">array: T&#91;], predicate: (item: T) =&gt; <span class=\"hljs-built_in\">boolean<\/span><\/span>): <span class=\"hljs-title\">T<\/span>&#91;] <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> array.filter(predicate);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now our return type is correctly inferred: it&#8217;s the exact same type that we pass in for the array. But we\u2019re only able to invoke it with a type that matches the&nbsp;<code>User<\/code>&nbsp;type, which is to say, has a&nbsp;<code>name<\/code>&nbsp;property that\u2019s a string.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"conditional-types\">Conditional Types<\/h3>\n\n\n\n<p>Conditional types allow us to, essentially, <em>ask questions<\/em> about types and form new types based on the answers.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">type<\/span> IsArray&lt;T&gt; = T <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-built_in\">any<\/span>&#91;] ? <span class=\"hljs-literal\">true<\/span> : <span class=\"hljs-literal\">false<\/span>;\n\n<span class=\"hljs-keyword\">type<\/span> YesIsArray = IsArray&lt;<span class=\"hljs-built_in\">number<\/span>&#91;]&gt;;\n<span class=\"hljs-keyword\">type<\/span> NoIsNotArray = IsArray&lt;<span class=\"hljs-built_in\">number<\/span>&gt;;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Here <code>YesIsArray<\/code> is the literal type <code>true<\/code> while <code>NoIsNotArray<\/code> is the literal type <code>false<\/code>. This is obviously pointless; the real value of conditional types usually comes with inferred types.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\">type ArrayOf<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">T<\/span>&gt;<\/span> = T extends Array<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">infer<\/span> <span class=\"hljs-attr\">U<\/span>&gt;<\/span> ? U : never;\n\ntype NumberType = ArrayOf<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">number&#91;]<\/span>&gt;<\/span>;\ntype NeverType = ArrayOf<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">number<\/span>&gt;<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Here the <code>Number<\/code> type is <code>number<\/code> and the NeverType type is, predictably, <code>never<\/code>. And yes, we can (and should) use generic constraints with these helper types<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">type ArrayOf2&lt;T extends <span class=\"hljs-built_in\">Array<\/span>&lt;any&gt;&gt; = T extends <span class=\"hljs-built_in\">Array<\/span>&lt;infer U&gt; ? U : never;\n\ntype NumberType2 = ArrayOf2&lt;number&#91;]&gt;;\ntype NeverType2 = ArrayOf2&lt;number&gt;;\n<span class=\"hljs-comment\">\/\/ ------------------------^^^^^^^<\/span>\n<span class=\"hljs-comment\">\/\/ Type 'number' does not satisfy the constraint 'any&#91;]'<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now we&#8217;re forbidden from using <code>ArrayOf2<\/code> with any type that&#8217;s not an array of something, so we&#8217;ll never have to worry about getting <code>never<\/code> back.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"lets-get-started\">Let&#8217;s Get Started<\/h2>\n\n\n\n<p>I recently wrote <a href=\"https:\/\/frontendmasters.com\/blog\/single-flight-mutations-in-tanstack-start-part-1\/\">a&nbsp;two-part post on single flight mutations<\/a>&nbsp;using <a href=\"https:\/\/tanstack.com\/start\/latest\">TanStack Start<\/a>. In order to make that work, we very carefully put together react-query options. Our query functions (which do the actual data fetching) were purposefully designed to be a single call against a TanStack Server Function. Then that same query function, as well as the argument payload it takes, were placed on react-query&#8217;s&nbsp;<code>meta<\/code>&nbsp;option.<\/p>\n\n\n\n<p>Then, in middleware on the server, we received query keys and looked up the server function and argument payload for a query so we could&nbsp;refetch its data.<\/p>\n\n\n\n<p>As part of those efforts, we built a simple helper to remove the duplication between the&nbsp;query function&nbsp;and the meta option.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">refetchedQueryOptions<\/span>(<span class=\"hljs-params\">queryKey: QueryKey, serverFn: <span class=\"hljs-built_in\">any<\/span>, arg?: <span class=\"hljs-built_in\">any<\/span><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> queryKeyToUse = &#91;...queryKey];\n  <span class=\"hljs-keyword\">if<\/span> (arg != <span class=\"hljs-literal\">null<\/span>) {\n    queryKeyToUse.push(arg);\n  }\n  <span class=\"hljs-keyword\">return<\/span> queryOptions({\n    queryKey: queryKeyToUse,\n    queryFn: <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n      <span class=\"hljs-keyword\">return<\/span> serverFn({ data: arg });\n    },\n    meta: {\n      __revalidate: {\n        serverFn,\n        arg,\n      },\n    },\n  });\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>It\u2019s a helper that takes in the query key, the server function, and argument payload, if any, and returns back&nbsp;<em>some<\/em>&nbsp;of our query options. It does this so the query function, and meta option will always be in sync with whatever server function is fetching our data. Then we compose it like this.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> epicsQueryOptions = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">page<\/span>: <span class=\"hljs-params\">number<\/span><\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">return<\/span> queryOptions({\n    ...refetchedQueryOptions(&#91;<span class=\"hljs-string\">\"epics\"<\/span>, <span class=\"hljs-string\">\"list\"<\/span>], getEpicsList, page),\n    staleTime: <span class=\"hljs-number\">1000<\/span> * <span class=\"hljs-number\">60<\/span> * <span class=\"hljs-number\">5<\/span>,\n    gcTime: <span class=\"hljs-number\">1000<\/span> * <span class=\"hljs-number\">60<\/span> * <span class=\"hljs-number\">5<\/span>,\n  });\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This proof-of-concept version worked fine, but nothing was typed. Our server function and argument payload were both marked as&nbsp;<code>any<\/code>, which didn\u2019t just fail to restrict invalid argument payloads, but also, disastrously, led all query hooks that used this to report the queried data as&nbsp;<code>any<\/code>.<\/p>\n\n\n\n<p>This post will implement a fully typed version of our <code>refetchedQueryOptions<\/code> function. It&#8217;s much harder than it might appear!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"our-success-criteria\">Our Success Criteria<\/h2>\n\n\n\n<p>Here&#8217;s our complete test setup.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">import<\/span> { QueryKey, queryOptions } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@tanstack\/react-query\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { createServerFn } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@tanstack\/react-start\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ ============================ Current Implementation ============================<\/span>\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">refetchedQueryOptions<\/span>(<span class=\"hljs-params\">queryKey: QueryKey, serverFn: <span class=\"hljs-built_in\">any<\/span>, arg?: <span class=\"hljs-built_in\">any<\/span><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> queryKeyToUse = &#91;...queryKey];\n  <span class=\"hljs-keyword\">if<\/span> (arg != <span class=\"hljs-literal\">null<\/span>) {\n    queryKeyToUse.push(arg);\n  }\n  <span class=\"hljs-keyword\">return<\/span> queryOptions({\n    queryKey: queryKeyToUse,\n    queryFn: <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n      <span class=\"hljs-keyword\">return<\/span> serverFn({ data: arg });\n    },\n    meta: {\n      __revalidate: {\n        serverFn,\n        arg,\n      },\n    },\n  });\n}\n\n<span class=\"hljs-comment\">\/\/ ============== Server Functions for testing ==============<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> serverFnWithArgs = createServerFn({ method: <span class=\"hljs-string\">\"GET\"<\/span> })\n  .inputValidator(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">arg<\/span>: { <span class=\"hljs-params\">value<\/span>: <span class=\"hljs-params\">string<\/span> }<\/span>) =&gt;<\/span> arg)\n  .handler(<span class=\"hljs-keyword\">async<\/span> () =&gt; {\n    <span class=\"hljs-keyword\">return<\/span> { value: <span class=\"hljs-string\">\"Hello World\"<\/span> };\n  });\n\n<span class=\"hljs-keyword\">const<\/span> serverFnWithoutArgs = createServerFn({ method: <span class=\"hljs-string\">\"GET\"<\/span> }).handler(<span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">return<\/span> { value: <span class=\"hljs-string\">\"Hello World\"<\/span> };\n});\n\n<span class=\"hljs-comment\">\/\/ ============================ Tests ============================<\/span>\n\nrefetchedQueryOptions(&#91;<span class=\"hljs-string\">\"test\"<\/span>], serverFnWithArgs, { value: <span class=\"hljs-string\">\"\"<\/span> });\nrefetchedQueryOptions(&#91;<span class=\"hljs-string\">\"test\"<\/span>], serverFnWithoutArgs);\n\n<span class=\"hljs-comment\">\/\/ wrong argument type<\/span>\n<span class=\"hljs-comment\">\/\/ FAILS - Unused '@ts-expect-error' directive.<\/span>\n<span class=\"hljs-comment\">\/\/ @ts-expect-error<\/span>\nrefetchedQueryOptions(&#91;<span class=\"hljs-string\">\"test\"<\/span>], serverFnWithArgs, <span class=\"hljs-number\">123<\/span>);\n\n<span class=\"hljs-comment\">\/\/ need an argument<\/span>\n<span class=\"hljs-comment\">\/\/ FAILS - Unused '@ts-expect-error' directive.<\/span>\n<span class=\"hljs-comment\">\/\/ @ts-expect-error<\/span>\nrefetchedQueryOptions(&#91;<span class=\"hljs-string\">\"test\"<\/span>], serverFnWithArgs);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>At the top we have the current iteration of our&nbsp;<code>refetchedQueryOptions<\/code>&nbsp;method. Beneath that, we have some server functions that will help us test this, one with an argument, the other without. And beneath that, we see four calls to <code>refetchedQueryOptions<\/code> to validate that our type checking is working properly. The top two we expect to succeed, and the bottom two we expect to error, which we verify with the <code>\/\/ @ts-expect-error<\/code> directive. This directive, well, <em>expects<\/em> an error on the very next line. If there is an error on the very next line, all is well; if there is no error on the next line, the <code>@ts-expect-error<\/code> directive will itself raise an error.<\/p>\n\n\n\n<p>Above, with our initial implementation, we see our expected errors fail to error out. This makes sense, since everything is typed as <code>any<\/code>, and our <code>arg<\/code> parameter is optional, so really anything goes.<\/p>\n\n\n\n<p>Even if you&#8217;re more than willing to live with imperfect typings, this current solution isn&#8217;t good for much. Since <code>serverFn<\/code> is typed as any, our <code>queryFn<\/code> will return <code>any<\/code>. That means any application code that&#8217;s using <code>useQuery<\/code> or <code>useSuspenseQuery<\/code> will now spit out <code>any<\/code> for your data.<\/p>\n\n\n\n<p>The rest of this post will get everything typed properly. We&#8217;ll have to do some unhinged things, so hopefully we&#8217;ll learn something new and maybe even have some fun.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"iteration-1\">Iteration 1<\/h2>\n\n\n\n<p>How&#8217;s this for a minimal improvement? Right now, the lack of a return type for the server function is absolutely killing us. Any usage of this query data will give use <code>any<\/code>. We <em>really<\/em> want our data properly typed in application code.<\/p>\n\n\n\n<p>TanStack Server functions are just&#8230; <em>functions<\/em>. They&#8217;re special in that you can call them from the client or the server, but at the end of the day, they&#8217;re functions. They always take in a single argument that has a <code>data<\/code> property for the standard arguments your function has defined (it also allows you to pass things like headers, but we won&#8217;t worry about that, here).<\/p>\n\n\n\n<p>Couldn&#8217;t we add a generic to our function, representing the server function? Once we have a function, we can use TypeScript&#8217;s built-in <code>Parameters<\/code> and <code>ReturnType<\/code> helpers. Let&#8217;s see what that looks like.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">refetchedQueryOptions<\/span>&lt;<span class=\"hljs-title\">T<\/span> <span class=\"hljs-title\">extends<\/span> (<span class=\"hljs-params\">arg: { data: <span class=\"hljs-built_in\">any<\/span> }<\/span>) =&gt; <span class=\"hljs-title\">Promise<\/span>&lt;<span class=\"hljs-title\">any<\/span>&gt;&gt;(<span class=\"hljs-params\"><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\"><span class=\"hljs-params\">  queryKey: QueryKey,<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\"><span class=\"hljs-params\">  serverFn: T,<\/span><\/span><\/span>\n<\/span><\/span><mark class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\"><span class=\"hljs-params\">  arg: Parameters&lt;T&gt;&#91;0]&#91;\"data\"],<\/span><\/span><\/span>\n<\/span><\/mark><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\"><span class=\"hljs-params\"><\/span>) <\/span>{<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">  <span class=\"hljs-keyword\">const<\/span> queryKeyToUse = &#91;...queryKey];<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">  <span class=\"hljs-keyword\">if<\/span> (arg != <span class=\"hljs-literal\">null<\/span>) {<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">    queryKeyToUse.push(arg);<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">  }<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">  <span class=\"hljs-keyword\">return<\/span> queryOptions({<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">    queryKey: queryKeyToUse,<\/span><\/span>\n<\/span><\/span><mark class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">    queryFn: <span class=\"hljs-keyword\">async<\/span> (): <span class=\"hljs-built_in\">Promise<\/span>&lt;Awaited&lt;ReturnType&lt;T&gt;&gt;&gt; =&gt; {<\/span><\/span>\n<\/span><\/mark><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">      <span class=\"hljs-keyword\">return<\/span> serverFn({ data: arg });<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">    },<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">    meta: {<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">      __revalidate: {<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">        serverFn,<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">        arg,<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">      },<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">    },<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">  });<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">}<\/span><\/span>\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We constrain our generic to be a function that takes in an <code>arg<\/code> with a <code>data<\/code> property. Moreover, we can now <em>use<\/em> our <code>T<\/code> generic in the parameter definition of <code>arg<\/code>, here <code>arg: Parameters&lt;T&gt;[0][\"data\"]<\/code>. Whatever our function is, we say that <code>arg<\/code> is the same type as the <code>data<\/code> property on the main argument that the function takes in.<\/p>\n\n\n\n<p>How does this look? Let&#8217;s check our tests<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">refetchedQueryOptions(&#91;<span class=\"hljs-string\">\"test\"<\/span>], serverFnWithArgs, { value: <span class=\"hljs-string\">\"\"<\/span> });\nrefetchedQueryOptions(&#91;<span class=\"hljs-string\">\"test\"<\/span>], serverFnWithoutArgs);\n<span class=\"hljs-comment\">\/\/ Error: Expected 3 arguments, but got 2.<\/span>\n\n<span class=\"hljs-comment\">\/\/ wrong argument type<\/span>\n<span class=\"hljs-comment\">\/\/ @ts-expect-error<\/span>\nrefetchedQueryOptions(&#91;<span class=\"hljs-string\">\"test\"<\/span>], serverFnWithArgs, <span class=\"hljs-number\">123<\/span>);\n\n<span class=\"hljs-comment\">\/\/ need an argument<\/span>\n<span class=\"hljs-comment\">\/\/ @ts-expect-error<\/span>\nrefetchedQueryOptions(&#91;<span class=\"hljs-string\">\"test\"<\/span>], serverFnWithArgs);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We have one problem. It seems we need to pass an argument for the query function which\u2026 doesn\u2019t take any parameters. It makes sense: <code>refetchedQueryOptions<\/code> does indeed define an <code>arg<\/code> parameter, which needs to be passed. I&#8217;ll be quick to note that simply passing <code>undefined<\/code> for that arg works perfectly.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">refetchedQueryOptions(&#91;<span class=\"hljs-string\">\"test\"<\/span>], serverFnWithoutArgs, <span class=\"hljs-literal\">undefined<\/span>);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This solves all our problems; our test code now has zero errors. For the vast, vast majority of apps, this will likely be fine. It&#8217;s entirely possible the work I&#8217;m about to show you to improve on this may not be worth the effort. <em>But<\/em>, going through that effort will likely teach us some neat things about TypeScript, and if we&#8217;re a special kind of strange, may even be fun.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"false-prophets\">False Prophets<\/h2>\n\n\n\n<p>You might think making arg optional would solve all our problems. Unfortunately, when we do that, <code>arg<\/code> becomes optional <em>everywhere<\/em>, including places we want to require it<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-20\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ need an argument<\/span>\n<span class=\"hljs-comment\">\/\/ FAILS - Unused '@ts-expect-error' directive.<\/span>\n<span class=\"hljs-comment\">\/\/ @ts-expect-error<\/span>\nrefetchedQueryOptions(&#91;<span class=\"hljs-string\">\"test\"<\/span>], serverFnWithArgs);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>If you&#8217;re an advanced TypeScript user you might think a conditional type is what we need. Detect the inferred arg type (what&#8217;s in the <code>data<\/code> arg), and if it&#8217;s not undefined, require it, but if it <em>is<\/em> undefined, then <em>don&#8217;t<\/em> require it. Unfortunately, there&#8217;s not really an easy way to represent &#8220;pass nothing&#8221; as the result of a conditional type. I&#8217;ve tried, and I was never able to get things fully working. I may have been missing something (feel free to drop a comment if you can figure it out), but even if there&#8217;s a trick to make it work, there&#8217;s a much more straightforward, idiomatic solution.<\/p>\n\n\n\n<p>We essentially want different function signatures in different circumstances: we want an arg when the server function we pass in takes an arg, and we want no arg when the server function we pass in takes no arg. Different function signatures is usually referred to as function overloading in computer science, and TypeScript supports this.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"function-overloading-in-typescript\">Function Overloading in TypeScript<\/h3>\n\n\n\n<p>As the simplest possible example, imagine you wanted to write an <code>add<\/code> function with two versions: one that takes in two numbers, and adds them; and one that takes in two strings, and concatenates them. Conceptually, we want this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-21\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">add<\/span>(<span class=\"hljs-params\">x: <span class=\"hljs-built_in\">number<\/span>, y: <span class=\"hljs-built_in\">number<\/span><\/span>): <span class=\"hljs-title\">number<\/span> <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> x + y;\n}\n\n<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">add<\/span>(<span class=\"hljs-params\">x: <span class=\"hljs-built_in\">string<\/span>, y: <span class=\"hljs-built_in\">string<\/span><\/span>): <span class=\"hljs-title\">string<\/span> <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> x + y;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>But that&#8217;s not valid; since JavaScript is a dynamically typed language, you can&#8217;t have more than one function of the same name, in the same scope. <span style=\"box-sizing: border-box; margin: 0px; padding: 0px;\"><em>TypeScript<\/em>&nbsp;does, however, allow us to overload functions, but the mechanics are a bit different.<\/span> Here&#8217;s how we do this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-22\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">add<\/span>(<span class=\"hljs-params\">x: <span class=\"hljs-built_in\">number<\/span>, y: <span class=\"hljs-built_in\">number<\/span><\/span>): <span class=\"hljs-title\">number<\/span><\/span>;\n<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">add<\/span>(<span class=\"hljs-params\">x: <span class=\"hljs-built_in\">string<\/span>, y: <span class=\"hljs-built_in\">string<\/span><\/span>): <span class=\"hljs-title\">string<\/span><\/span>;\n<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">add<\/span>(<span class=\"hljs-params\">x: <span class=\"hljs-built_in\">string<\/span> | <span class=\"hljs-built_in\">number<\/span>, y: <span class=\"hljs-built_in\">string<\/span> | <span class=\"hljs-built_in\">number<\/span><\/span>): <span class=\"hljs-title\">string<\/span> | <span class=\"hljs-title\">number<\/span> <\/span>{\n  <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">typeof<\/span> x === <span class=\"hljs-string\">\"string\"<\/span> &amp;&amp; <span class=\"hljs-keyword\">typeof<\/span> y === <span class=\"hljs-string\">\"string\"<\/span>) {\n    <span class=\"hljs-keyword\">return<\/span> x + y;\n  }\n  <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">typeof<\/span> x === <span class=\"hljs-string\">\"number\"<\/span> &amp;&amp; <span class=\"hljs-keyword\">typeof<\/span> y === <span class=\"hljs-string\">\"number\"<\/span>) {\n    <span class=\"hljs-keyword\">return<\/span> x + y;\n  }\n  <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">\"Invalid arguments\"<\/span>);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-22\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We start with the function <em>definitions<\/em>. This one:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-23\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">add<\/span>(<span class=\"hljs-params\">x: <span class=\"hljs-built_in\">number<\/span>, y: <span class=\"hljs-built_in\">number<\/span><\/span>): <span class=\"hljs-title\">number<\/span><\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-23\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And this one:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-24\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">add<\/span>(<span class=\"hljs-params\">x: <span class=\"hljs-built_in\">string<\/span>, y: <span class=\"hljs-built_in\">string<\/span><\/span>): <span class=\"hljs-title\">string<\/span><\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-24\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>These define the actual API of our function. We declare that this function can take in two numbers and return a number, or two strings and return a string.<\/p>\n\n\n\n<p>Then we have the actual implementation of the function.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-25\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">add<\/span>(<span class=\"hljs-params\">x: <span class=\"hljs-built_in\">string<\/span> | <span class=\"hljs-built_in\">number<\/span>, y: <span class=\"hljs-built_in\">string<\/span> | <span class=\"hljs-built_in\">number<\/span><\/span>): <span class=\"hljs-title\">string<\/span> | <span class=\"hljs-title\">number<\/span> <\/span>{\n  <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">typeof<\/span> x === <span class=\"hljs-string\">\"string\"<\/span> &amp;&amp; <span class=\"hljs-keyword\">typeof<\/span> y === <span class=\"hljs-string\">\"string\"<\/span>) {\n    <span class=\"hljs-keyword\">return<\/span> x + y;\n  }\n  <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">typeof<\/span> x === <span class=\"hljs-string\">\"number\"<\/span> &amp;&amp; <span class=\"hljs-keyword\">typeof<\/span> y === <span class=\"hljs-string\">\"number\"<\/span>) {\n    <span class=\"hljs-keyword\">return<\/span> x + y;\n  }\n  <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">\"Invalid arguments\"<\/span>);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-25\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The inputs and return types all have to be a union of every definition. In other words, the actual implementation has to accept any of the definitions.<\/p>\n\n\n\n<p>And now, when we try to call this function, we only see the definitions available to us.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"674\" height=\"174\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/02\/img1.png?resize=674%2C174&#038;ssl=1\" alt=\"Code snippet showing TypeScript function definition for adding two numbers, with parameters x and y typed as number.\" class=\"wp-image-8533\" style=\"width:554px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/02\/img1.png?w=674&amp;ssl=1 674w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/02\/img1.png?resize=300%2C77&amp;ssl=1 300w\" sizes=\"auto, (max-width: 674px) 100vw, 674px\" \/><\/figure>\n<\/div>\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"678\" height=\"188\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/02\/img2.png?resize=678%2C188&#038;ssl=1\" alt=\"\" class=\"wp-image-8534\" style=\"width:556px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/02\/img2.png?w=678&amp;ssl=1 678w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/02\/img2.png?resize=300%2C83&amp;ssl=1 300w\" sizes=\"auto, (max-width: 678px) 100vw, 678px\" \/><\/figure>\n<\/div>\n\n\n<p>The implementation is a little weird. You might wonder why we need<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-26\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">\"Invalid arguments\"<\/span>);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-26\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The only valid invocations for this function are two strings or two numbers; that&#8217;s all TypeScript will allow. So why does TypeScript require us to have that throw at the end? If both arguments are not strings, and neither argument is a number, the function will never be allowed. Unfortunately, TypeScript isn&#8217;t quite smart enough to understand that. The function implementation has x and y both as <code>string | number<\/code> so as far as it&#8217;s concerned, <code>x<\/code> could be a string and <code>y<\/code> could be a number. Understanding that this combination is disallowed by the prior overload definitions isn&#8217;t currently within TypeScript&#8217;s capabilities.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"building-our-solution\">Building Our Solution<\/h2>\n\n\n\n<p>So we want to overload <code>refetchedQueryOptions<\/code> twice: once for a server function that takes in an argument, and once for a server function that takes no arguments. How do we define either case? This is where things get fun.<\/p>\n\n\n\n<p>To start, let&#8217;s define a type representing any async function<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-27\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">type<\/span> AnyAsyncFn = <span class=\"hljs-function\">(<span class=\"hljs-params\">...<span class=\"hljs-params\">args<\/span>: <span class=\"hljs-params\">any<\/span>&#91;]<\/span>) =&gt;<\/span> <span class=\"hljs-built_in\">Promise<\/span>&lt;<span class=\"hljs-built_in\">any<\/span>&gt;;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-27\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This seems like a waste of time, but it&#8217;ll save us some typing and add a lot of clarity soon.<\/p>\n\n\n\n<p>Let\u2019s define a type that takes in an async function and just strips out the argument type. A conditional type is perfect for this. We saw something similar before with a conditional type that strips out the type of an array&#8217;s elements.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-28\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">type<\/span> ArrayOf&lt;T <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-built_in\">Array<\/span>&lt;<span class=\"hljs-built_in\">any<\/span>&gt;&gt; = T <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-built_in\">Array<\/span>&lt;infer U&gt; ? U : never;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-28\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We check that T extends an array, and then we plopped <code>infer U<\/code> right into the generic slot the Array type already has. Let&#8217;s do something similar to get the parameter type of an async function.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-29\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">type<\/span> ServerFnArgs&lt;TFn <span class=\"hljs-keyword\">extends<\/span> AnyAsyncFn&gt; = Parameters&lt;TFn&gt;&#91;<span class=\"hljs-number\">0<\/span>] <span class=\"hljs-keyword\">extends<\/span> { data: infer TResult } ? TResult : <span class=\"hljs-literal\">undefined<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-29\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>There&#8217;s a <code>Parameters&lt;T&gt;<\/code> type that can pluck parameters out of a function type. We grab the zero&#8217;th parameter (functions can have multiple parameters, but server functions only have one). On that single, 0th parameter, look for a <code>data<\/code> property, and if present, infer that. Otherwise return undefined.<\/p>\n\n\n\n<p>From there we can start to ask questions about our types.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-30\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">type<\/span> ServerFnHasArgs&lt;TFn <span class=\"hljs-keyword\">extends<\/span> AnyAsyncFn&gt; = ServerFnArgs&lt;TFn&gt; <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-literal\">undefined<\/span> ? <span class=\"hljs-literal\">false<\/span> : <span class=\"hljs-literal\">true<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-30\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And we can then make other type helpers.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-31\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">type<\/span> ServerFnWithArgs&lt;TFn <span class=\"hljs-keyword\">extends<\/span> AnyAsyncFn&gt; = ServerFnHasArgs&lt;TFn&gt; <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-literal\">true<\/span> ? TFn : never;\n<span class=\"hljs-keyword\">type<\/span> ServerFnWithoutArgs&lt;TFn <span class=\"hljs-keyword\">extends<\/span> AnyAsyncFn&gt; = ServerFnHasArgs&lt;TFn&gt; <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-literal\">false<\/span> ? TFn : never;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-31\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We&#8217;ve built some helper types that take a function type in, and tests whether that function has, or does not have server function arguments.<\/p>\n\n\n\n<p>One major bummer of TypeScript overloading is that we can&#8217;t rely on inferred return types, so we&#8217;ll have to define our return type manually.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-32\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">type<\/span> RefetchQueryOptions&lt;T&gt; = {\n  queryKey: QueryKey;\n  queryFn: <span class=\"hljs-function\">(<span class=\"hljs-params\">_?: <span class=\"hljs-params\">any<\/span><\/span>) =&gt;<\/span> <span class=\"hljs-built_in\">Promise<\/span>&lt;T&gt;;\n  meta: <span class=\"hljs-built_in\">any<\/span>;\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-32\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And with that, we should be ready to define our overload signatures.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-33\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">refetchedQueryOptions<\/span>&lt;<span class=\"hljs-title\">TFn<\/span> <span class=\"hljs-title\">extends<\/span> <span class=\"hljs-title\">AnyAsyncFn<\/span>&gt;(<span class=\"hljs-params\">\n  queryKey: QueryKey,\n  serverFn: ServerFnWithArgs&lt;TFn&gt;,\n  arg: Parameters&lt;TFn&gt;&#91;0]&#91;\"data\"],\n<\/span>): <span class=\"hljs-title\">RefetchQueryOptions<\/span>&lt;<span class=\"hljs-title\">Awaited<\/span>&lt;<span class=\"hljs-title\">ReturnType<\/span>&lt;<span class=\"hljs-title\">TFn<\/span>&gt;&gt;&gt;<\/span>;\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">refetchedQueryOptions<\/span>&lt;<span class=\"hljs-title\">TFn<\/span> <span class=\"hljs-title\">extends<\/span> <span class=\"hljs-title\">AnyAsyncFn<\/span>&gt;(<span class=\"hljs-params\">\n  queryKey: QueryKey,\n  serverFn: ServerFnWithoutArgs&lt;TFn&gt;,\n<\/span>): <span class=\"hljs-title\">RefetchQueryOptions<\/span>&lt;<span class=\"hljs-title\">Awaited<\/span>&lt;<span class=\"hljs-title\">ReturnType<\/span>&lt;<span class=\"hljs-title\">TFn<\/span>&gt;&gt;&gt;<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-33\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>One version for a Server Function that takes an argument, as well as the argument, and a version for a Server Function that takes no argument, with no such argument passed.<\/p>\n\n\n\n<p>The full implementation:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-34\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">refetchedQueryOptions<\/span>&lt;<span class=\"hljs-title\">TFn<\/span> <span class=\"hljs-title\">extends<\/span> <span class=\"hljs-title\">AnyAsyncFn<\/span>&gt;(<span class=\"hljs-params\">\n  queryKey: QueryKey,\n  serverFn: ServerFnWithoutArgs&lt;TFn&gt; | ServerFnWithArgs&lt;TFn&gt;,\n  arg?: Parameters&lt;TFn&gt;&#91;0]&#91;\"data\"],\n<\/span>): <span class=\"hljs-title\">RefetchQueryOptions<\/span>&lt;<span class=\"hljs-title\">Awaited<\/span>&lt;<span class=\"hljs-title\">ReturnType<\/span>&lt;<span class=\"hljs-title\">TFn<\/span>&gt;&gt;&gt; <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> queryKeyToUse = &#91;...queryKey];\n  <span class=\"hljs-keyword\">if<\/span> (arg != <span class=\"hljs-literal\">null<\/span>) {\n    queryKeyToUse.push(arg);\n  }\n  <span class=\"hljs-keyword\">return<\/span> {\n    queryKey: queryKeyToUse,\n    queryFn: <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n      <span class=\"hljs-keyword\">return<\/span> serverFn({ data: arg });\n    },\n    meta: {\n      __revalidate: {\n        serverFn,\n        arg,\n      },\n    },\n  };\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-34\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And that&#8217;s that.<\/p>\n\n\n\n<p>Generics, combined with conditional types, can make for an incredibly powerful combination. When you look at things the right way, you can ask very useful questions about your types that allow you to build the precise API you want.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"concluding-thoughts\">Concluding Thoughts<\/h2>\n\n\n\n<p>I hope this deep dive into a niche use case has taught you at least something useful about TypeScript. Even if you never need to solve this particular problem \u2014 and let&#8217;s face it, you probably won&#8217;t \u2014 these tools and skills are widely applicable.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Generics, combined with conditional types can make for an incredibly powerful combination. When you look at things the right way, you can ask very useful questions about your types that allow you to build the precise API you want.<\/p>\n","protected":false},"author":21,"featured_media":8569,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[450,184],"class_list":["post-8524","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-generics","tag-typescript"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/02\/type.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/8524","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/21"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=8524"}],"version-history":[{"count":17,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/8524\/revisions"}],"predecessor-version":[{"id":8603,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/8524\/revisions\/8603"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/8569"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=8524"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=8524"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=8524"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}