{"id":294,"date":"2026-05-30T13:02:40","date_gmt":"2026-05-30T13:02:40","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/typescript-satisfies-when-i-stopped-reaching-for-as\/"},"modified":"2026-05-30T13:02:40","modified_gmt":"2026-05-30T13:02:40","slug":"typescript-satisfies-when-i-stopped-reaching-for-as","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/typescript-satisfies-when-i-stopped-reaching-for-as\/","title":{"rendered":"TypeScript satisfies: When I Stopped Reaching for as"},"content":{"rendered":"<p>Short version for the impatient: <code>satisfies<\/code> is the TypeScript keyword I now reach for whenever I want the compiler to check that a value matches a type, without throwing away what the compiler already knows about that value. If you&rsquo;ve ever typed <code>as SomeType<\/code> and then sighed because you knew you were lying to the compiler a little, this is for you.<\/p>\n<p>I was reviewing a pull request from someone on my team last week and saw three <code>as Foo<\/code> casts on a config object. The author wasn&rsquo;t wrong, exactly, but every one of those casts was a tiny bit of trust the compiler was being asked to give up. I left a single comment that just said <code>try satisfies<\/code>. The diff that came back was shorter, safer, and the IDE got smarter about autocomplete inside the object. That&rsquo;s the post.<\/p>\n<h2 id=\"why-as-always-made-me-a-little-nervous\">Why <code>as<\/code> always made me a little nervous<\/h2>\n<p>I&rsquo;ll start with my honest opinion of <code>as<\/code>. It&rsquo;s a hammer. It tells the compiler &ldquo;trust me, I know what this is.&rdquo; It works. It&rsquo;s also been the cause of approximately every prod incident I&rsquo;ve shipped that wasn&rsquo;t a missing await.<\/p>\n<p>The usual pattern looks like this:<\/p>\n<pre><code class=\"language-ts\">type Theme = {\n  light: { bg: string; fg: string };\n  dark: { bg: string; fg: string };\n};\n\nconst theme = {\n  light: { bg: &quot;#fff&quot;, fg: &quot;#111&quot; },\n  dark:  { bg: &quot;#0b0b0b&quot;, fg: &quot;#eaeaea&quot; },\n} as Theme;\n<\/code><\/pre>\n<p>This compiles. It also silently allows you to write <code>dark: { bg: \"#0b0b0b\" }<\/code> (missing <code>fg<\/code>) and never hear about it, because <code>as<\/code> widens the assertion. The compiler doesn&rsquo;t check that your literal actually fits the type. It just nods and walks away.<\/p>\n<p>The other usual fix is a type annotation:<\/p>\n<pre><code class=\"language-ts\">const theme: Theme = {\n  light: { bg: &quot;#fff&quot;, fg: &quot;#111&quot; },\n  dark:  { bg: &quot;#0b0b0b&quot;, fg: &quot;#eaeaea&quot; },\n};\n<\/code><\/pre>\n<p>Now the compiler will yell if <code>fg<\/code> is missing. But you&rsquo;ve also told it the type is <code>Theme<\/code>, which means <code>theme.light.bg<\/code> is just <code>string<\/code>, not the literal <code>\"#fff\"<\/code>. If you wanted to derive a union of allowed background colours from this object, you&rsquo;ve lost that info. Annoying.<\/p>\n<h2 id=\"what-satisfies-actually-does\">What <code>satisfies<\/code> actually does<\/h2>\n<p><code>satisfies<\/code>, <a href=\"https:\/\/devblogs.microsoft.com\/typescript\/announcing-typescript-4-9\/\" rel=\"nofollow noopener\" target=\"_blank\">introduced in TypeScript 4.9<\/a>, splits those two jobs apart. It checks that the value fits the type. It does not change the inferred type of the value.<\/p>\n<pre><code class=\"language-ts\">const theme = {\n  light: { bg: &quot;#fff&quot;, fg: &quot;#111&quot; },\n  dark:  { bg: &quot;#0b0b0b&quot;, fg: &quot;#eaeaea&quot; },\n} satisfies Theme;\n\n\/\/ theme.light.bg is now &quot;#fff&quot;, not string\n\/\/ missing fg in dark is now a compile error\n<\/code><\/pre>\n<p>That&rsquo;s the whole feature. Read it twice, because it took me a couple of months to internalise that those are different things.<\/p>\n<p>The <a href=\"https:\/\/www.typescriptlang.org\/docs\/handbook\/release-notes\/typescript-4-9.html#the-satisfies-operator\" rel=\"nofollow noopener\" target=\"_blank\">TypeScript handbook page on <code>satisfies<\/code><\/a> has the canonical example with palettes and hex codes. It&rsquo;s fine. But the version that finally got it into my head was config objects, which is what I&rsquo;ll use here.<\/p>\n<h2 id=\"where-it-earns-its-keep-in-my-codebase\">Where it earns its keep in my codebase<\/h2>\n<p>I now use <code>satisfies<\/code> in three places without thinking, and I want to walk through them quickly because nobody else writes about these as a group.<\/p>\n<h3 id=\"route-or-feature-maps\">Route or feature maps<\/h3>\n<p>A Next.js app I worked on had a routes map that looked like this:<\/p>\n<pre><code class=\"language-ts\">const routes = {\n  home: &quot;\/&quot;,\n  blog: &quot;\/blog&quot;,\n  post: (slug: string) =&gt; `\/blog\/${slug}`,\n} as const;\n<\/code><\/pre>\n<p>This was fine until someone wanted a type that said &ldquo;the name of any route&rdquo;. With a <code>Record&lt;string, ...&gt;<\/code> annotation we&rsquo;d have lost the literal keys. With <code>as Record&lt;...&gt;<\/code> we&rsquo;d have lost the function signatures. With <code>satisfies<\/code>, both are preserved:<\/p>\n<pre><code class=\"language-ts\">type RouteValue = string | ((...args: never[]) =&gt; string);\n\nconst routes = {\n  home: &quot;\/&quot;,\n  blog: &quot;\/blog&quot;,\n  post: (slug: string) =&gt; `\/blog\/${slug}`,\n} as const satisfies Record&lt;string, RouteValue&gt;;\n\ntype RouteName = keyof typeof routes; \/\/ &quot;home&quot; | &quot;blog&quot; | &quot;post&quot;\n<\/code><\/pre>\n<p>Now <code>RouteName<\/code> is the actual union of keys, the function is still callable with <code>(slug: string)<\/code> and not <code>(...args: never[])<\/code>, and the compiler will yell if I add a value that isn&rsquo;t a string or a function returning a string. That&rsquo;s three useful things from one keyword.<\/p>\n<h3 id=\"api-response-shape-guards-in-tests\">API response shape guards in tests<\/h3>\n<p>When I write a fixture for an API test, I want the fixture to be the real shape of the response, and I want the compiler to scream if the real type changes. <code>satisfies<\/code> makes the fixture self-checking:<\/p>\n<pre><code class=\"language-ts\">import type { GetUserResponse } from &quot;.\/api-types&quot;;\n\nexport const fakeUser = {\n  id: &quot;usr_123&quot;,\n  email: &quot;abrar@example.com&quot;,\n  roles: [&quot;admin&quot;, &quot;editor&quot;],\n} satisfies GetUserResponse;\n<\/code><\/pre>\n<p>If I later add a required <code>createdAt<\/code> to <code>GetUserResponse<\/code>, the fixture breaks at compile time, not three months later in a flake. I&rsquo;ve shipped at least two bugs because a test fixture was cast with <code>as<\/code> and silently went out of date. Never again, hopefully.<\/p>\n<h3 id=\"discriminated-unions-that-look-obvious-but-arent\">Discriminated unions that look obvious but aren&rsquo;t<\/h3>\n<p>This one is mine. I had a state object like <code>{ status: \"loading\" } | { status: \"ready\", data: T }<\/code>. When I built the initial value, I&rsquo;d often write <code>as const<\/code> and call it a day. The problem is <code>as const<\/code> doesn&rsquo;t check anything, it just narrows. So you can produce an initial value that doesn&rsquo;t fit your union and TypeScript will happily generate a tighter literal type that conflicts with what you actually expect.<\/p>\n<pre><code class=\"language-ts\">type State = { status: &quot;loading&quot; } | { status: &quot;ready&quot;; data: User };\n\nconst initial = { status: &quot;loading&quot; } as const satisfies State;\n<\/code><\/pre>\n<p>The <code>as const<\/code> keeps <code>status<\/code> as the literal <code>\"loading\"<\/code>. The <code>satisfies State<\/code> makes sure I haven&rsquo;t accidentally written something the union doesn&rsquo;t allow. The two-keyword combo (<code>as const satisfies<\/code>) is now muscle memory for me on every reducer file.<\/p>\n<h2 id=\"where-i-still-dont-bother\">Where I still don&rsquo;t bother<\/h2>\n<p>There are two cases where <code>satisfies<\/code> is overkill and I just don&rsquo;t use it.<\/p>\n<p>The first is single-use literals inside function arguments. If I&rsquo;m calling <code>setOptions({ retries: 3 })<\/code> once, the inline literal already gets contextually typed against the parameter. Adding <code>satisfies<\/code> here is noise.<\/p>\n<p>The second is when I genuinely need to coerce a value across a type boundary the compiler can&rsquo;t reason about, usually around external library types or <code>JSON.parse<\/code>. That&rsquo;s still an <code>as<\/code> cast, and I&rsquo;d rather have one obvious lie than a <code>satisfies<\/code> that gives me a false sense of safety. I cover that style of &ldquo;defensive cast&rdquo; pattern more in <a href=\"https:\/\/abrarqasim.com\/blog\/typescript-generics-in-production-what-i-actually-reach-for\/\" rel=\"noopener\">my notes on TypeScript generics in production<\/a>.<\/p>\n<h2 id=\"a-small-pitfall-i-hit-twice\">A small pitfall I hit twice<\/h2>\n<p><code>satisfies<\/code> checks the value against the type, not the other way around. So if your value has <em>extra<\/em> properties, the compiler will complain about excess property checks when you&rsquo;d rather it didn&rsquo;t. For library config objects that accept extra forward-compatible keys, you sometimes still want a regular annotation. I keep a <code>\/\/ keep the annotation here, satisfies is too strict<\/code> comment in two spots for this exact reason.<\/p>\n<p>Also, please don&rsquo;t go around the codebase replacing every <code>:<\/code> with <code>satisfies<\/code>. The vast majority of typed variables in a normal app are fine with a plain annotation. <code>satisfies<\/code> shines when you care about preserving the <em>exact<\/em> value type, which is mostly true for constants, fixtures, and config maps.<\/p>\n<h2 id=\"what-id-do-this-week\">What I&rsquo;d do this week<\/h2>\n<p>If you&rsquo;re on TypeScript 4.9 or newer, grep your codebase for <code>as<\/code> outside of test files and JSX. Look for the patterns I described, config objects, route maps, fixtures, initial state, and try <code>satisfies<\/code> instead. You&rsquo;ll probably delete a few quiet casts and gain better autocomplete in the process.<\/p>\n<p>If you write a lot of typed configs and want a second pair of eyes on a real codebase, that&rsquo;s the kind of work I take on as a freelancer; <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">you can see some recent client work here<\/a>. And if <code>satisfies<\/code> finally makes a tricky union click for you, I&rsquo;d love to hear which one.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>How I actually use TypeScript&#8217;s satisfies operator in real code, where it beats type annotations and as casts, and the two places I still don&#8217;t bother.<\/p>\n","protected":false},"author":2,"featured_media":293,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"How I actually use TypeScript's satisfies operator in real code, where it beats type annotations and as casts, and the two places I still don't bother.","rank_math_focus_keyword":"typescript satisfies","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[156],"tags":[98,38,63],"class_list":["post-294","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-typescript","tag-developer-tools","tag-frontend","tag-typescript"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/294","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=294"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/294\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/293"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=294"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=294"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=294"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}