{"id":324,"date":"2026-06-13T13:03:18","date_gmt":"2026-06-13T13:03:18","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/typescript-utility-types-i-actually-use-and-the-ones-i-skip\/"},"modified":"2026-06-13T13:03:18","modified_gmt":"2026-06-13T13:03:18","slug":"typescript-utility-types-i-actually-use-and-the-ones-i-skip","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/typescript-utility-types-i-actually-use-and-the-ones-i-skip\/","title":{"rendered":"TypeScript Utility Types I Actually Use (And the Ones I Skip)"},"content":{"rendered":"<p>Confession: for about two years I maintained a file called <code>types\/user-inputs.ts<\/code> that was just the <code>User<\/code> interface copied four times with small edits. <code>CreateUserInput<\/code>. <code>UpdateUserInput<\/code>. <code>PublicUser<\/code>. <code>UserPreview<\/code>. Every time the real <code>User<\/code> grew a field, I&rsquo;d update it and then forget at least one of the copies. The bug would surface weeks later, three layers away, as an API response missing a field that the compiler swore was fine.<\/p>\n<p>The fix had been sitting in the language the whole time. TypeScript utility types take one source-of-truth type and derive the variants, so when the source changes, the variants follow. I knew they existed. I&rsquo;d just filed them under &ldquo;fancy type-level stuff I&rsquo;ll learn later,&rdquo; next to conditional types and whatever <code>infer<\/code> does.<\/p>\n<p>This post is the tour I wish someone had given me: the utility types I now use weekly, the hand-rolled junk they replaced, and the clever ones I tried and quietly backed out of.<\/p>\n<h2 id=\"what-utility-types-actually-are\">What utility types actually are<\/h2>\n<p>Nothing magic. They&rsquo;re mapped and conditional types that ship with the compiler, documented on one page of the <a href=\"https:\/\/www.typescriptlang.org\/docs\/handbook\/utility-types.html\" rel=\"nofollow noopener\" target=\"_blank\">official handbook<\/a>. You could write most of them yourself in a line or two. The value isn&rsquo;t sophistication, it&rsquo;s that they&rsquo;re standard: every TypeScript developer who reads <code>Partial&lt;User&gt;<\/code> knows instantly what it means, and nobody has to review your homemade <code>MakeOptional&lt;T&gt;<\/code> for correctness.<\/p>\n<p>That standardization argument is what finally sold me. My hand-rolled versions weren&rsquo;t wrong. They were just one more thing for the next person to read, doubt, and re-verify.<\/p>\n<h2 id=\"partial-pick-and-omit-do-most-of-the-work\">Partial, Pick and Omit do most of the work<\/h2>\n<p>Here&rsquo;s the duplicated-interface mess I used to write:<\/p>\n<pre><code class=\"language-ts\">interface User {\n  id: string;\n  email: string;\n  name: string;\n  passwordHash: string;\n  createdAt: Date;\n}\n\n\/\/ the drift-prone copies\ninterface UpdateUserInput {\n  email?: string;\n  name?: string;\n}\n\ninterface PublicUser {\n  id: string;\n  email: string;\n  name: string;\n  createdAt: Date;\n}\n<\/code><\/pre>\n<p>And the derived version that replaced it:<\/p>\n<pre><code class=\"language-ts\">type UpdateUserInput = Partial&lt;Pick&lt;User, &quot;email&quot; | &quot;name&quot;&gt;&gt;;\ntype PublicUser = Omit&lt;User, &quot;passwordHash&quot;&gt;;\n<\/code><\/pre>\n<p>Two lines, and they can&rsquo;t drift. Add a field to <code>User<\/code> and <code>PublicUser<\/code> picks it up on the next compile. Rename <code>passwordHash<\/code> and the <code>Omit<\/code> breaks loudly instead of leaking quietly.<\/p>\n<p>A warning about <code>Partial<\/code> before you spray it everywhere, because I did and regretted it. <code>Partial&lt;User&gt;<\/code> makes every field optional, including ones your update logic actually requires. My update endpoint happily accepted <code>{}<\/code> for months. Nothing crashed. It just did nothing, successfully, with a 200 status. The pattern that fixed it: keep <code>Partial<\/code> for the fields that are legitimately optional and intersect the required ones back in.<\/p>\n<pre><code class=\"language-ts\">type UpdateUserInput = Partial&lt;Pick&lt;User, &quot;email&quot; | &quot;name&quot;&gt;&gt; &amp; {\n  id: string; \/\/ you can't update nothing and nobody\n};\n<\/code><\/pre>\n<p><code>Partial<\/code> is a scalpel. Used on a narrow <code>Pick<\/code>, it&rsquo;s precise. Used on a whole entity, it tells the compiler &ldquo;anything goes,&rdquo; and the compiler believes you.<\/p>\n<p>One habit worth stealing: prefer <code>Pick<\/code> over <code>Omit<\/code> for anything security-adjacent. <code>Omit<\/code> is a denylist. If someone adds a <code>resetToken<\/code> field to <code>User<\/code> next quarter, <code>Omit&lt;User, \"passwordHash\"&gt;<\/code> will happily include it in your public type. <code>Pick<\/code> is an allowlist, so new fields stay private until you opt them in. I learned that one the embarrassing way, though thankfully in a code review rather than an incident report.<\/p>\n<h2 id=\"record-and-the-index-signature-it-replaced\">Record, and the index signature it replaced<\/h2>\n<p>I used to type lookup objects like this:<\/p>\n<pre><code class=\"language-ts\">const statusColors: { [key: string]: string } = {\n  active: &quot;green&quot;,\n  suspended: &quot;amber&quot;,\n  deleted: &quot;red&quot;,\n};\n<\/code><\/pre>\n<p>The index signature accepts any string, which means <code>statusColors.actve<\/code> compiles fine and returns <code>undefined<\/code> at runtime. The <code>Record<\/code> version closes that hole:<\/p>\n<pre><code class=\"language-ts\">type Status = &quot;active&quot; | &quot;suspended&quot; | &quot;deleted&quot;;\n\nconst statusColors: Record&lt;Status, string&gt; = {\n  active: &quot;green&quot;,\n  suspended: &quot;amber&quot;,\n  deleted: &quot;red&quot;,\n};\n<\/code><\/pre>\n<p>Now a typo&rsquo;d key is a compile error, and so is a missing one. Add <code>\"banned\"<\/code> to the <code>Status<\/code> union and the compiler walks you to every lookup table that needs the new entry. It turns &ldquo;did I update everything?&rdquo; from a memory exercise into a build failure, and my memory loses that contest often enough that I&rsquo;ll take the trade every time.<\/p>\n<h2 id=\"returntype-and-awaited-for-code-you-dont-control\">ReturnType and Awaited, for code you don&rsquo;t control<\/h2>\n<p>The workhorses above shine on your own types. This pair shines on everyone else&rsquo;s. Some library exports a function but not the type of what it returns. Old me would reconstruct that shape by hand, fifteen lines of interface transcribed from dumped output, stale the moment the library updated.<\/p>\n<pre><code class=\"language-ts\">\/\/ the type the library never exported\ntype Session = Awaited&lt;ReturnType&lt;typeof auth.getSession&gt;&gt;;\n<\/code><\/pre>\n<p><code>ReturnType<\/code> extracts what the function returns, <code>Awaited<\/code> unwraps the promise, and the result tracks the library exactly. When the package updates and the shape changes, my code breaks at compile time instead of in production. I use the same trick on my own database query helpers, where the row shape lives in one query function and everything downstream derives from it.<\/p>\n<p><code>Parameters<\/code> is the same idea pointed at inputs, and it quietly cleaned up my test files. I had test helpers that re-declared a service function&rsquo;s argument types, which meant every signature change broke the tests in the dumbest possible way: not because behavior changed, but because the helper&rsquo;s copy of the types went stale.<\/p>\n<pre><code class=\"language-ts\">function buildOrderArgs(\n  overrides: Partial&lt;Parameters&lt;typeof createOrder&gt;[0]&gt; = {}\n) {\n  return { customerId: &quot;test-1&quot;, items: [], ...overrides };\n}\n<\/code><\/pre>\n<p>Now the helper&rsquo;s types are the function&rsquo;s types. Change <code>createOrder<\/code> and the helpers either keep working or fail at the exact line that needs attention. My test maintenance time on that service dropped enough that I went back and did the same thing to two other projects the same week. I wrote about a related habit in <a href=\"https:\/\/abrarqasim.com\/blog\/typescript-satisfies-when-i-stopped-reaching-for-as\" rel=\"noopener\">my satisfies post<\/a>, and the theme is the same: stop telling the compiler things it already knows.<\/p>\n<h2 id=\"the-ones-i-tried-and-put-back\">The ones I tried and put back<\/h2>\n<p>Honesty section. Not every utility type earned a spot.<\/p>\n<p><code>Capitalize<\/code>, <code>Uncapitalize<\/code> and the string-manipulation family are impressive and I have used them exactly once outside of a toy, generating event-handler names from event names. The type errors they produce when something goes wrong are genuinely hard to read, and a junior teammate lost an afternoon to one of mine. If a plain union written out by hand costs me ten keystrokes and saves the next person that afternoon, the union wins.<\/p>\n<p><code>NoInfer<\/code>, added in <a href=\"https:\/\/devblogs.microsoft.com\/typescript\/announcing-typescript-5-4\/\" rel=\"nofollow noopener\" target=\"_blank\">TypeScript 5.4<\/a>, solves a real inference problem in generic function signatures. If you maintain a library, learn it. In application code I&rsquo;ve needed it maybe twice, and both times rearranging my function arguments solved the same problem more legibly.<\/p>\n<p>Deeply nested <code>Omit&lt;Pick&lt;Partial&lt;...&gt;&gt;&gt;<\/code> chains are the type-level version of clever one-liners. When I find myself three utilities deep, that&rsquo;s usually the type telling me the underlying model is wrong, and the fix belongs in the data design, not the type gymnastics. The same instinct applies to generics, which I covered in <a href=\"https:\/\/abrarqasim.com\/blog\/typescript-generics-in-production-what-i-actually-reach-for\" rel=\"noopener\">what I actually reach for with TypeScript generics<\/a>: the cleverness budget is real and it&rsquo;s smaller than you think.<\/p>\n<h2 id=\"something-to-try-this-week\">Something to try this week<\/h2>\n<p>Grep your codebase for interfaces that share three or more field names with another interface. Each cluster is a candidate for one source type plus <code>Pick<\/code>, <code>Omit<\/code> or <code>Partial<\/code> derivations. Convert one cluster, then add a field to the source type and watch where the compiler takes you. That single compile error tour usually converts people faster than any blog post, including this one.<\/p>\n<p>I do this audit early on most <a href=\"https:\/\/abrarqasim.com\" rel=\"noopener\">client projects I take on<\/a>, and it&rsquo;s reliably the cheapest win in the codebase: no runtime change, no new dependency, just deleting copies that were waiting to drift.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Six TypeScript utility types that earn their keep in production, the hand-rolled types they replaced, and the clever ones I stopped using. Real code included.<\/p>\n","protected":false},"author":2,"featured_media":323,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"Six TypeScript utility types that earn their keep in production, the hand-rolled types they replaced, and the clever ones I stopped using. Real code included.","rank_math_focus_keyword":"typescript utility types","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[45],"tags":[38,44,367,63,366],"class_list":["post-324","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-programming","tag-frontend","tag-javascript","tag-type-safety-2","tag-typescript","tag-typescript-utility-types"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/324","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=324"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/324\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/323"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=324"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=324"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=324"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}