{"id":246,"date":"2026-05-18T05:00:42","date_gmt":"2026-05-18T05:00:42","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/typescript-generics-in-production-what-i-actually-reach-for\/"},"modified":"2026-05-18T05:00:42","modified_gmt":"2026-05-18T05:00:42","slug":"typescript-generics-in-production-what-i-actually-reach-for","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/typescript-generics-in-production-what-i-actually-reach-for\/","title":{"rendered":"TypeScript Generics in Production: What I Actually Reach For"},"content":{"rendered":"<p>Confession: I avoided TypeScript generics for the first eight months I used the language. I&rsquo;d write <code>function getUser(id: string): any<\/code> and tell myself I&rsquo;d fix it later. I never did. Then I read a PR from a coworker who&rsquo;d taken twelve lines of my code, replaced it with three lines and one generic, and made the autocomplete actually work. That was the moment.<\/p>\n<p>Generics are one of those things where the docs make them look harder than they are, and once they click, you wonder why you were so afraid. I&rsquo;ve been writing TypeScript professionally for years now, and there are three patterns I reach for over and over. I want to write them down, partly so the juniors on my team have something to point at, partly because writing them down helps me notice when I&rsquo;m overusing them.<\/p>\n<p>If you want the canonical syntax reference, the <a href=\"https:\/\/www.typescriptlang.org\/docs\/handbook\/2\/generics.html\" rel=\"nofollow noopener\" target=\"_blank\">TypeScript handbook on generics<\/a> covers it. This is the field guide.<\/p>\n<h2 id=\"pattern-1-keep-the-input-type-through-the-function\">Pattern 1: keep the input type through the function<\/h2>\n<p>The most common place generics actually pay off: a helper that takes some object and returns part of it. Without a generic, the type information at the call site disappears.<\/p>\n<p>Here&rsquo;s the bad version I see in code review at least once a week:<\/p>\n<pre><code class=\"language-ts\">function pick(obj: any, keys: string[]): any {\n  const out: any = {};\n  for (const k of keys) out[k] = obj[k];\n  return out;\n}\n<\/code><\/pre>\n<p>It compiles. It&rsquo;s also a lie. The caller has no idea what they got back, autocomplete dies, typos slip through. Three <code>any<\/code>s in one function is the kind of code smell I learned to spot from a mile away.<\/p>\n<p>The generic version:<\/p>\n<pre><code class=\"language-ts\">function pick&lt;T extends object, K extends keyof T&gt;(\n  obj: T,\n  keys: K[],\n): Pick&lt;T, K&gt; {\n  const out = {} as Pick&lt;T, K&gt;;\n  for (const k of keys) out[k] = obj[k];\n  return out;\n}\n<\/code><\/pre>\n<p>Call it with <code>pick(user, ['email', 'name'])<\/code> and you get back <code>{ email: string; name: string }<\/code>. Try <code>pick(user, ['emial'])<\/code> and the compiler stops you. That&rsquo;s it.<\/p>\n<p>The two pieces doing the work: <code>K extends keyof T<\/code> forces the keys to actually exist on the input, and the built-in <code>Pick&lt;T, K&gt;<\/code> utility type builds the return shape so I don&rsquo;t have to. I use this exact pattern for response slicing and prop forwarding in React. Anywhere I&rsquo;m passing a subset of an object through.<\/p>\n<p>One trick worth knowing: if you call <code>pick(user, ['email', 'name'])<\/code>, TypeScript infers <code>K<\/code> as the literal union <code>'email' | 'name'<\/code>, not <code>string<\/code>. That literal inference is what makes the return type tight. If you ever wrap this call in a function that takes <code>string[]<\/code> and forwards it, you lose the literal union and the safety collapses back to <code>Record&lt;string, unknown&gt;<\/code>-ish. The lesson I learned the hard way: don&rsquo;t widen too early.<\/p>\n<h2 id=\"pattern-2-derive-the-output-type-with-infer\">Pattern 2: derive the output type with infer<\/h2>\n<p>This is the one that took me longest to like. The <code>infer<\/code> keyword looked cryptic until I had a real problem that needed it.<\/p>\n<p>The problem was a thin wrapper that unwrapped a Promise if you handed it one, and passed through the value otherwise. Writing the runtime is easy. Writing the return type is where I got stuck.<\/p>\n<pre><code class=\"language-ts\">type Awaited&lt;T&gt; = T extends Promise&lt;infer U&gt; ? U : T;\n<\/code><\/pre>\n<p>Read it slowly. &ldquo;If T looks like Promise<something>, give me that something. Otherwise give me T.&rdquo; That&rsquo;s the whole shape. Conditional types pattern-match like a switch statement, and <code>infer<\/code> is how you grab the piece you need.<\/p>\n<p><code>Awaited<\/code> is now in the standard lib, so you don&rsquo;t have to write it yourself. But once that shape clicked for me, every conditional type I&rsquo;d seen in library code suddenly made sense.<\/p>\n<p>A more real example, an event bus where each handler gets the right payload typed automatically:<\/p>\n<pre><code class=\"language-ts\">type Events = {\n  'user:created': { id: string; email: string };\n  'order:shipped': { orderId: string; tracking: string };\n  'payment:failed': { orderId: string; reason: string };\n};\n\nfunction on&lt;E extends keyof Events&gt;(\n  event: E,\n  handler: (payload: Events[E]) =&gt; void,\n) {\n  \/\/ wires through to whatever event emitter you use\n}\n\non('user:created', (payload) =&gt; {\n  \/\/ payload is { id: string; email: string }\n  console.log(payload.email);\n});\n<\/code><\/pre>\n<p>Add a new event to the <code>Events<\/code> map and every call site picks up the right handler type. The compiler walks the map for you, no casts needed.<\/p>\n<p>Another infer pattern I reach for: pulling the argument type out of an existing function so I don&rsquo;t have to retype it.<\/p>\n<pre><code class=\"language-ts\">type FirstArg&lt;F&gt; = F extends (arg: infer A, ...rest: any[]) =&gt; any ? A : never;\n\nfunction logSomeServiceCall(arg: FirstArg&lt;typeof someService.create&gt;) {\n  console.log('would call with', arg);\n}\n<\/code><\/pre>\n<p>If <code>someService.create<\/code> changes signature, <code>logSomeServiceCall<\/code> follows along. Without <code>infer<\/code>, I&rsquo;d be duplicating the input type and watching it drift.<\/p>\n<p>For the deeper end of conditional types, the <a href=\"https:\/\/www.typescriptlang.org\/docs\/handbook\/2\/conditional-types.html\" rel=\"nofollow noopener\" target=\"_blank\">TypeScript page on conditional types<\/a> covers <code>infer<\/code> properly, including distributive conditional types which I&rsquo;ll spare you here.<\/p>\n<h2 id=\"pattern-3-constraints-that-catch-the-wrong-shape\">Pattern 3: constraints that catch the wrong shape<\/h2>\n<p>I used to write generics with no constraint, then add runtime checks to compensate. Wrong order. The constraint is the check.<\/p>\n<p>Tiny example. I needed a helper that turned a list of items into a map keyed by id:<\/p>\n<pre><code class=\"language-ts\">function byId&lt;T extends { id: string }&gt;(items: T[]): Record&lt;string, T&gt; {\n  const out: Record&lt;string, T&gt; = {};\n  for (const item of items) out[item.id] = item;\n  return out;\n}\n<\/code><\/pre>\n<p>The <code>extends { id: string }<\/code> part is the whole point. Hand it an array of items without an <code>id<\/code> field, the compiler stops you before the code runs. Hand it <code>User[]<\/code> and you get <code>Record&lt;string, User&gt;<\/code> back. No runtime guard needed.<\/p>\n<p>The mistake I see in junior code: people write <code>T extends any[]<\/code> or <code>T extends object<\/code> thinking they&rsquo;re being permissive. A constraint of <code>object<\/code> is barely tighter than no constraint at all. The useful constraints are specific shapes like <code>{ id: string }<\/code> or <code>Record&lt;string, unknown&gt;<\/code>.<\/p>\n<p>Rule of thumb I use: if the constraint isn&rsquo;t telling the compiler something it couldn&rsquo;t already infer, drop it.<\/p>\n<h2 id=\"where-i-stop-using-generics\">Where I stop using generics<\/h2>\n<p>This is the part most tutorials skip. There&rsquo;s a phase after you learn generics where you start sprinkling them everywhere, even on functions that only ever see one type. Code review gets slower because every signature now requires you to mentally substitute <code>&lt;T&gt;<\/code> to read the body.<\/p>\n<p>Places I now actively delete generics. Internal helpers with one caller: if <code>processOrder<\/code> is only ever called with <code>Order<\/code>, just type the parameter <code>Order<\/code>. The generic adds noise without adding safety.<\/p>\n<p>React components where the prop type doesn&rsquo;t actually vary. I keep seeing <code>&lt;Button&lt;T&gt;&gt;<\/code> style components where the generic is never bound to anything dynamic. Use a union, or write two components.<\/p>\n<p>Functions where the generic is decorative. If the constraint is so loose that the caller can pass anything, the generic isn&rsquo;t earning its keep. Same with output types: when the return type is <code>T | undefined<\/code> regardless of what <code>T<\/code> is, the generic doesn&rsquo;t add information.<\/p>\n<p>The test I run on myself before adding <code>&lt;T&gt;<\/code>: would deleting the generic make the call site less safe? If the answer is no, the generic is decoration.<\/p>\n<p>A concrete refactor I did last quarter. I had a helper that looked like this:<\/p>\n<pre><code class=\"language-ts\">function logEvent&lt;T&gt;(name: string, payload: T): void {\n  analytics.track(name, payload);\n}\n<\/code><\/pre>\n<p>Looked clever. Did nothing. The constraint on <code>T<\/code> was missing, so <code>payload<\/code> could be anything, and there was no return type to derive. Every call site already typed the payload object literal. I deleted the <code>&lt;T&gt;<\/code> and typed <code>payload: Record&lt;string, unknown&gt;<\/code>. Same safety, less syntax. Nobody on the team noticed because nothing changed for them.<\/p>\n<p>I covered something adjacent to this in my <a href=\"https:\/\/abrarqasim.com\/blog\/typescript-satisfies-when-i-reach-for-it\/\" rel=\"noopener\">post on the satisfies operator<\/a>. A lot of the time, the cleaner answer is a normal type plus <code>satisfies<\/code>, not a generic at all. If you&rsquo;ve been reaching for <code>&lt;T&gt;<\/code> reflexively, that one&rsquo;s worth a read too.<\/p>\n<h2 id=\"what-to-try-this-week\">What to try this week<\/h2>\n<p>Pick one helper in your codebase that currently uses <code>any<\/code> or returns a loosely-typed object. Rewrite it using Pattern 1, the one with <code>T extends object, K extends keyof T<\/code>. Read the call site the next morning. If autocomplete improved and the function got safer, keep it. If the signature got harder to read without buying anything, revert.<\/p>\n<p>Generics are a tool for keeping type information accurate when data flows through helpers. They aren&rsquo;t a status symbol. The three patterns here cover most of the TypeScript I actually ship. If you want to see the kind of codebases I apply this stuff in, <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">my project work is here<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Three TypeScript generics patterns I actually use in production code, plus the places I learned to delete the generic and what I write instead.<\/p>\n","protected":false},"author":2,"featured_media":245,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"Three TypeScript generics patterns I actually use in production code, plus the places I learned to delete the generic and what I write instead.","rank_math_focus_keyword":"typescript generics","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[45],"tags":[286,285,50,63],"class_list":["post-246","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-programming","tag-developer-experience","tag-generics","tag-programming","tag-typescript"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/246","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=246"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/246\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/245"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=246"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=246"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=246"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}