<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: nyaomaru</title>
    <description>The latest articles on DEV Community by nyaomaru (@nyaomaru).</description>
    <link>https://dev.to/nyaomaru</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1436158%2Fa78bc4ed-7796-4760-baa5-06283c2404d6.png</url>
      <title>DEV Community: nyaomaru</title>
      <link>https://dev.to/nyaomaru</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nyaomaru"/>
    <language>en</language>
    <item>
      <title>How I Made JavaScript Execution Visual and Rewindable (DSA View View 👀👀)</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Wed, 29 Jul 2026 10:42:13 +0000</pubDate>
      <link>https://dev.to/nyaomaru/how-i-made-javascript-execution-visual-and-rewindable-dsa-view-view--42k4</link>
      <guid>https://dev.to/nyaomaru/how-i-made-javascript-execution-visual-and-rewindable-dsa-view-view--42k4</guid>
      <description>&lt;p&gt;Hoi hoi!&lt;/p&gt;

&lt;p&gt;I'm &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;@nyaomaru&lt;/a&gt;, a frontend engineer who was recently &lt;strong&gt;knocked off my feet by how good the zeetong (sole) from a Dutch fish shop was&lt;/strong&gt;. 🐟😸&lt;/p&gt;

&lt;p&gt;In the previous article, I explained how DSA View View takes the &lt;code&gt;TypeScript&lt;/code&gt; written by a user, analyzes the function or class signature, and generates a Verification form.&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/nyaomaru/how-i-built-the-editor-and-compile-pipeline-for-a-typescript-dsa-visualizer-dsa-view-view--530f" class="crayons-story__hidden-navigation-link"&gt;How I Built the Editor and Compile Pipeline for a TypeScript DSA Visualizer (DSA View View 👀👀)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/nyaomaru" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1436158%2Fa78bc4ed-7796-4760-baa5-06283c2404d6.png" alt="nyaomaru profile" class="crayons-avatar__image" width="800" height="790"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/nyaomaru" class="crayons-story__secondary fw-medium m:hidden"&gt;
              nyaomaru
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                nyaomaru
                
              
              &lt;div id="story-author-preview-content-4197911" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/nyaomaru" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1436158%2Fa78bc4ed-7796-4760-baa5-06283c2404d6.png" class="crayons-avatar__image" alt="" width="800" height="790"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;nyaomaru&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/nyaomaru/how-i-built-the-editor-and-compile-pipeline-for-a-typescript-dsa-visualizer-dsa-view-view--530f" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 22&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/nyaomaru/how-i-built-the-editor-and-compile-pipeline-for-a-typescript-dsa-visualizer-dsa-view-view--530f" id="article-link-4197911"&gt;
          How I Built the Editor and Compile Pipeline for a TypeScript DSA Visualizer (DSA View View 👀👀)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/typescript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;typescript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/react"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;react&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/nyaomaru/how-i-built-the-editor-and-compile-pipeline-for-a-typescript-dsa-visualizer-dsa-view-view--530f" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;32&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/nyaomaru/how-i-built-the-editor-and-compile-pipeline-for-a-typescript-dsa-visualizer-dsa-view-view--530f#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            10 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;This time, we are entering the heart of DSA View View: the &lt;strong&gt;Runtime&lt;/strong&gt; and &lt;strong&gt;Visualization&lt;/strong&gt; layers.&lt;/p&gt;

&lt;p&gt;If you call an ordinary JavaScript function, the direct result you get back is usually the final return value.&lt;/p&gt;

&lt;p&gt;But when I am studying DSA, that is not the part I really want to see.&lt;/p&gt;

&lt;p&gt;I want to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What happened in the middle of the loop?&lt;/li&gt;
&lt;li&gt;When did the pointer move?&lt;/li&gt;
&lt;li&gt;At which exact moment did the variable change?&lt;/li&gt;
&lt;li&gt;How deep did the recursion go?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, I want to see &lt;strong&gt;the journey to the answer&lt;/strong&gt;, not only the answer itself.&lt;/p&gt;

&lt;p&gt;JavaScript, unfortunately, does not provide an API that accepts:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Excuse me, could you move the current line one step backward?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So how does DSA View View rewind execution?&lt;/p&gt;

&lt;p&gt;Here is the answer up front. 😸&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Before execution, DSA View View rewrites the code into a version that can record its intermediate state, then runs it until it completes or reaches a configured limit.&lt;/p&gt;

&lt;p&gt;The UI then plays those recorded states forward and backward like security-camera footage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this article, we will look at that &lt;strong&gt;record-and-replay&lt;/strong&gt; system through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AST transformation with Babel&lt;/li&gt;
&lt;li&gt;Injecting &lt;code&gt;recordStep&lt;/code&gt; calls&lt;/li&gt;
&lt;li&gt;Preserving the original meaning of the code&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ExecutionStep&lt;/code&gt; and snapshot patches&lt;/li&gt;
&lt;li&gt;Rewinding and jumping to variable changes&lt;/li&gt;
&lt;li&gt;Running code inside a Web Worker&lt;/li&gt;
&lt;li&gt;Detecting the appropriate Visualization&lt;/li&gt;
&lt;li&gt;Tests that protect the AST transformation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source code is available here 👇&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;
        nyaomaru
      &lt;/a&gt; / &lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;
        dsa-view-view
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      DSA View View allows you to understand DSA to see the data flow. 👀👀 Of course, it's free.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/nyaomaru/dsa-view-view/./public/readme-view-view.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fdsa-view-view%2FHEAD%2F.%2Fpublic%2Freadme-view-view.png" alt="DSA View View" width="100%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;DSA View View&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;DSA View View turns TypeScript algorithm functions into step-by-step visual
stories. 👀👀&lt;/p&gt;
&lt;p&gt;&lt;a href="https://dsa-view-view.vercel.app/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fdsa-view-view%2FHEAD%2F.%2Fpublic%2Fdemo%2Fdsa-view-view-tv.gif" alt="DSA View View TV"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Write code, run it with structured inputs, and see the arrays, matrices
trees, lists, stacks, pointers, and return values move as the function executes.&lt;/p&gt;
&lt;p&gt;It is built for those moments when reading the code is not enough and you want
to &lt;em&gt;view&lt;/em&gt; why the answer changes.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://dsa-view-view.vercel.app/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fdsa-view-view%2FHEAD%2F.%2Fpublic%2Fdemo%2Fdsa-view-view-demo.gif" alt="DSA View View demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why Try It?&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;🧠 &lt;strong&gt;Step through real TypeScript&lt;/strong&gt;&lt;br&gt;
Paste or edit a function, validate it, then run the exact code in the browser.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🧩 &lt;strong&gt;Views that match the data&lt;/strong&gt;&lt;br&gt;
Arrays become bars, matrices become grids, trees become node graphs, linked lists become chains, and two-pointer area problems get their own visual view.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🌳 &lt;strong&gt;DSA-friendly inputs out of the box&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;TreeNode&lt;/code&gt;, &lt;code&gt;ListNode&lt;/code&gt;, &lt;code&gt;MinHeap&lt;/code&gt;, &lt;code&gt;MaxHeap&lt;/code&gt;, &lt;code&gt;PriorityQueue&lt;/code&gt;, nested arrays
matrices, strings, numbers, and class-style inputs are supported without
ceremony.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🔎 &lt;strong&gt;39 built-in examples&lt;/strong&gt;&lt;br&gt;
Search by name, browse…&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;All right, let’s open the Runtime and see what Mr. View has been watching. 👀👀&lt;/p&gt;




&lt;h2&gt;
  
  
  📝 The Big Picture: Turning Execution into an Array of Steps
&lt;/h2&gt;

&lt;p&gt;The Runtime pipeline looks roughly like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User TypeScript
      ↓
Prepare common DSA classes
      ↓
Parse the code into an AST with Babel Parser
      ↓
Visitors inject recordStep(...)
      ↓
Transform TypeScript into JavaScript with Babel
      ↓
Execute it with new Function inside a Web Worker
      ↓
Generate ExecutionStep[]
      ↓
React moves currentStep forward and backward
      ↓
Display the Visualization that matches the trace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important design decision here is that &lt;strong&gt;the user’s code is not connected directly to a React component&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead, &lt;code&gt;ExecutionStep[]&lt;/code&gt; acts as a shared recording format.&lt;/p&gt;

&lt;p&gt;That lets arrays, pointers, recursion, trees, heaps, and other structures all live on the same timeline.&lt;/p&gt;

&lt;p&gt;Let’s walk through how that works.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔭 Runtime: Planting Observation Points in the Code
&lt;/h2&gt;

&lt;p&gt;This is the heart of DSA View View. 💖&lt;/p&gt;

&lt;p&gt;The Runtime needs intermediate states, so it does not execute the user’s code exactly as written.&lt;/p&gt;

&lt;p&gt;First, Babel transforms the AST and injects calls to &lt;code&gt;recordStep&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Injecting &lt;code&gt;recordStep&lt;/code&gt; into the AST
&lt;/h3&gt;

&lt;p&gt;Suppose the user writes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DSA View View parses it into an AST and transforms it into something roughly like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;recordStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;variable-declaration&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;let left = 0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;recordStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loop-iteration&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;while (left &amp;lt; right)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;recordStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assignment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;left++&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The second argument to &lt;code&gt;recordStep&lt;/code&gt; is the original source line number, not the execution-step number.&lt;br&gt;
Because line 2 is empty, the recorded lines are &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;3&lt;/code&gt;, and &lt;code&gt;4&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the real implementation, dedicated visitors handle AST nodes such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FunctionDeclaration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FunctionExpression&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ArrowFunctionExpression&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ClassMethod&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VariableDeclaration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AssignmentExpression&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UpdateExpression&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CallExpression&lt;/code&gt; nodes that mutate arrays&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;for&lt;/code&gt; / &lt;code&gt;while&lt;/code&gt; / &lt;code&gt;for...of&lt;/code&gt; / &lt;code&gt;for...in&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReturnStatement&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A step can therefore be recorded when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A function is entered&lt;/li&gt;
&lt;li&gt;A variable is declared&lt;/li&gt;
&lt;li&gt;A loop completes an iteration&lt;/li&gt;
&lt;li&gt;A value is assigned&lt;/li&gt;
&lt;li&gt;An array is mutated&lt;/li&gt;
&lt;li&gt;A function returns&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Current limitation&lt;/strong&gt;&lt;br&gt;
callback bodies inside methods such as &lt;code&gt;sort&lt;/code&gt;, &lt;code&gt;reduce&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, and &lt;code&gt;forEach&lt;/code&gt; are intentionally excluded from instrumentation for now.&lt;/p&gt;

&lt;p&gt;The goal is not to trace every corner of JavaScript immediately. I am starting with patterns that are useful for DSA traces and can be instrumented without breaking the original behavior.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Injecting the Code Is Easier Than Not Breaking It
&lt;/h3&gt;

&lt;p&gt;The hardest part of the AST transformation was not calling &lt;code&gt;recordStep&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The hard part was making sure that,&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;after adding the recording logic, every original expression is still evaluated the same number of times and still returns the same value.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example, if a return expression were evaluated once for recording and once for the actual return, its side effects would run twice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;someFunction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the transformed code first stores the value in a temporary variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;algorithmVisualizerReturnValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;someFunction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;recordStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;return&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;return value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;algorithmVisualizerReturnValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;return location&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;returnLocation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;algorithmVisualizerReturnValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assignment expressions are another fun little trap.&lt;/p&gt;

&lt;p&gt;Consider this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transformed code must preserve all three facts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;i++&lt;/code&gt; runs exactly once&lt;/li&gt;
&lt;li&gt;The pre-increment value of &lt;code&gt;i&lt;/code&gt; is used as the array index&lt;/li&gt;
&lt;li&gt;The assignment expression itself evaluates to &lt;code&gt;7&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DSA View View wraps the assignment in a synthetic arrow function, stores the result in a temporary variable, records the step, and returns the same value.&lt;/p&gt;

&lt;p&gt;Using an arrow function also avoids accidentally stealing the surrounding &lt;code&gt;this&lt;/code&gt; or &lt;code&gt;arguments&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;AST transformation cannot be "close enough".&lt;/p&gt;

&lt;p&gt;If adding more visible steps changes the answer, the visualizer has somehow become the least trustworthy character in the story.&lt;/p&gt;

&lt;p&gt;Not ideal.&lt;/p&gt;




&lt;h2&gt;
  
  
  🖍️ &lt;code&gt;ExecutionStep&lt;/code&gt;: Making Execution Replayable
&lt;/h2&gt;

&lt;p&gt;The Runtime records each observed state as an &lt;code&gt;ExecutionStep&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A shortened version of the real type looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ExecutionStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;stepNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExecutionStepType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;column&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;callStack&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;loopIteration&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;conditionResult&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;heapTrace&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;HeapTraceSnapshot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Runtime currently generates steps including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;function-call&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;function-entry&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;variable-declaration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;assignment&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;array-mutation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;loop-iteration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;line&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; are used in the timeline.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;variables&lt;/code&gt; provides the data needed to reconstruct the UI at that moment.&lt;/p&gt;

&lt;p&gt;Recursive traces also use &lt;code&gt;callStack&lt;/code&gt;, while prepared heap implementations can attach &lt;code&gt;metadata.heapTrace&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Saving References Would Rewrite the Past
&lt;/h3&gt;

&lt;p&gt;Suppose an array reference were stored directly inside a step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nf"&gt;recordStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* save stack */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the recorded step keeps the same reference, the earlier state will also appear as &lt;code&gt;[0]&lt;/code&gt; after &lt;code&gt;pop()&lt;/code&gt; runs.&lt;/p&gt;

&lt;p&gt;That is not a historical record.&lt;/p&gt;

&lt;p&gt;That is the past quietly editing itself.&lt;/p&gt;

&lt;p&gt;So values passed to &lt;code&gt;recordStep&lt;/code&gt; are deep-cloned at that moment.&lt;/p&gt;

&lt;p&gt;The Runtime normally uses &lt;code&gt;structuredClone&lt;/code&gt;. When it encounters values that need different handling, it falls back to a cloning implementation that supports cycles, &lt;code&gt;Map&lt;/code&gt;, &lt;code&gt;Set&lt;/code&gt;, &lt;code&gt;Date&lt;/code&gt;, &lt;code&gt;RegExp&lt;/code&gt;, and other cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "Delta Snapshot" Is Really More Like a Patch
&lt;/h3&gt;

&lt;p&gt;The implementation contains a name called &lt;code&gt;variableDeltas&lt;/code&gt;, but this deserves a more precise explanation.&lt;/p&gt;

&lt;p&gt;The current Runtime does &lt;strong&gt;not&lt;/strong&gt; compare every value with the previous state and store only the keys that truly changed.&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The first step stores the initial state, including the inputs&lt;/li&gt;
&lt;li&gt;Later steps deep-clone the visible variables passed to that &lt;code&gt;recordStep&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;When a full snapshot is needed, the patches are merged in order&lt;/li&gt;
&lt;li&gt;Reconstructed snapshots are cached&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So, strictly speaking, these are closer to &lt;strong&gt;per-step patches&lt;/strong&gt; than minimal deltas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Step 0 patch: { left: 0, right: 5 }
Step 1 patch: { left: 1 }
Step 2 patch: { right: 4 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reconstructing them produces.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Step 0 snapshot: { left: 0, right: 5 }
Step 1 snapshot: { left: 1, right: 5 }
Step 2 snapshot: { left: 1, right: 4 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;variables&lt;/code&gt; property of each &lt;code&gt;ExecutionStep&lt;/code&gt; is a getter.&lt;/p&gt;

&lt;p&gt;It starts from a previously cached snapshot and merges only the patches needed to reach the requested step.&lt;/p&gt;

&lt;p&gt;There is one more implementation detail worth mentioning.&lt;/p&gt;

&lt;p&gt;The Runtime sends state from the Worker to the UI with &lt;code&gt;postMessage&lt;/code&gt;. During that structured clone, reading the getter also materializes the snapshot.&lt;/p&gt;

&lt;p&gt;So the data does not remain a tiny delta forever and magically arrive on the main thread in that form.&lt;/p&gt;

&lt;p&gt;The design mainly reduces eager reconstruction and cloning &lt;strong&gt;while the execution is being recorded&lt;/strong&gt;. 📽️&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Can the Timeline Move Backward?
&lt;/h3&gt;

&lt;p&gt;DSA View View is &lt;strong&gt;not reversing JavaScript while it runs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Inside the Web Worker, it first rewrites the code into an instrumented version and records the execution.&lt;/p&gt;

&lt;p&gt;During playback, the main value that changes is &lt;code&gt;currentStep&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ExecutionState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;currentStep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;totalSteps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExecutionStep&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;isComplete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;returnValue&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Moving forward displays the snapshot for &lt;code&gt;currentStep + 1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Moving backward displays the snapshot for &lt;code&gt;currentStep - 1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So rewinding means,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;selecting an earlier recorded frame, not undoing the execution itself&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once recording has finished, users can navigate without rerunning their code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pause&lt;/li&gt;
&lt;li&gt;Move one step forward or backward&lt;/li&gt;
&lt;li&gt;Jump to any step in the timeline&lt;/li&gt;
&lt;li&gt;Move to the first or final step&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Users can also select a variable and jump directly to the previous step where its value changed.&lt;/p&gt;

&lt;p&gt;That is especially useful for pointers such as &lt;code&gt;i&lt;/code&gt;, &lt;code&gt;left&lt;/code&gt;, and &lt;code&gt;right&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No more clicking the Back button 47 times while whispering, "Where did you go wrong?" 😸&lt;/p&gt;

&lt;h3&gt;
  
  
  There Is a Generator, but It Does Not Suspend the User’s Code Line by Line
&lt;/h3&gt;

&lt;p&gt;The Runtime API contains a &lt;code&gt;Generator&amp;lt;ExecutionStep, ...&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That might make it look as if the user’s code itself is being paused line by line with &lt;code&gt;yield&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is not.&lt;/p&gt;

&lt;p&gt;After yielding the first &lt;code&gt;function-call&lt;/code&gt; step, the instrumented code runs once and accumulates steps inside the execution context.&lt;/p&gt;

&lt;p&gt;The generator then yields the already-recorded steps in order.&lt;/p&gt;

&lt;p&gt;In the path used by the UI, the Worker consumes the generator completely and returns the finished &lt;code&gt;ExecutionState&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Separating &lt;strong&gt;execution&lt;/strong&gt; from &lt;strong&gt;playback&lt;/strong&gt; is what makes the rewind behavior relatively simple.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ Web Worker: Do Not Take the UI Down with You
&lt;/h2&gt;

&lt;p&gt;User code runs inside a dedicated Web Worker instead of the main thread.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./execution-worker.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;algorithm-execution&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An infinite loop or expensive computation on the main thread would freeze the entire page.&lt;/p&gt;

&lt;p&gt;By moving execution into a Worker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The React UI stays on the main thread&lt;/li&gt;
&lt;li&gt;Starting a new run or leaving the page can terminate the Worker through an &lt;code&gt;AbortController&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Runtime also has limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Execution timeout: 10 seconds&lt;/li&gt;
&lt;li&gt;Maximum recorded steps: 3,000&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a limit is reached, the Worker can be terminated or execution can stop early with a warning.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Worker Is Not a Security Sandbox
&lt;/h3&gt;

&lt;p&gt;A Web Worker does not have access to the page DOM, &lt;code&gt;localStorage&lt;/code&gt;, or &lt;code&gt;sessionStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;DSA View View also does not execute user code on a server.&lt;/p&gt;

&lt;p&gt;However, &lt;strong&gt;a Web Worker is not a security boundary that makes untrusted code safe.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The user code runs through &lt;code&gt;new Function&lt;/code&gt; in the Worker global scope.&lt;/p&gt;

&lt;p&gt;Web APIs available inside Workers, including &lt;code&gt;fetch&lt;/code&gt;, are not automatically disabled.&lt;/p&gt;

&lt;p&gt;The purpose of this architecture is to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Isolate heavy execution from the main thread&lt;/li&gt;
&lt;li&gt;Terminate an execution unit on timeout or cancellation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is an execution boundary for responsiveness and control, not a magical prison for JavaScript.&lt;/p&gt;

&lt;p&gt;JavaScript has watched enough escape-room videos to know better.&lt;/p&gt;




&lt;h2&gt;
  
  
  👀 Visualization: Variables Alone Are Not Enough
&lt;/h2&gt;

&lt;p&gt;The Runtime returns an &lt;code&gt;ExecutionState&lt;/code&gt; and snapshots for each step.&lt;/p&gt;

&lt;p&gt;The next problem is deciding which View should display that trace.&lt;/p&gt;

&lt;p&gt;DSA View View does not ask AI,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Hey, does this look like Binary Search?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;on every run.&lt;/p&gt;

&lt;p&gt;Instead, visualization candidates are detected through logic based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Variable names&lt;/li&gt;
&lt;li&gt;Variable types and data structures&lt;/li&gt;
&lt;li&gt;Whether arrays or matrices actually changed&lt;/li&gt;
&lt;li&gt;Pointers such as &lt;code&gt;left&lt;/code&gt;, &lt;code&gt;right&lt;/code&gt;, and &lt;code&gt;mid&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The call stack&lt;/li&gt;
&lt;li&gt;Function-entry and return history&lt;/li&gt;
&lt;li&gt;Heap metadata&lt;/li&gt;
&lt;li&gt;Changes across multiple steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, displaying a Sort Graph whenever a &lt;code&gt;number[]&lt;/code&gt; exists would classify algorithms that only &lt;em&gt;read&lt;/em&gt; an input array as sorting algorithms.&lt;/p&gt;

&lt;p&gt;So the detection logic checks whether the numeric array actually changes between steps.&lt;/p&gt;

&lt;p&gt;It also looks for context such as &lt;code&gt;sort&lt;/code&gt;, &lt;code&gt;swap&lt;/code&gt;, &lt;code&gt;partition&lt;/code&gt;, and &lt;code&gt;pivot&lt;/code&gt; in descriptions or the call stack.&lt;/p&gt;

&lt;p&gt;Some representative mappings are 👇&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime state&lt;/th&gt;
&lt;th&gt;Displayed View&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A changing numeric array&lt;/td&gt;
&lt;td&gt;Sort Graph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An array used as a stack&lt;/td&gt;
&lt;td&gt;Stack View&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search using &lt;code&gt;left&lt;/code&gt; / &lt;code&gt;right&lt;/code&gt; / &lt;code&gt;mid&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Index View&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sliding Window pointers&lt;/td&gt;
&lt;td&gt;Sliding Window View&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A two-dimensional array&lt;/td&gt;
&lt;td&gt;Matrix View&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DP arrays or rolling state&lt;/td&gt;
&lt;td&gt;DP View&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TreeNode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tree Graph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ListNode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List Graph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recursive function entry and return&lt;/td&gt;
&lt;td&gt;Recursion Tree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logic using &lt;code&gt;Map&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Map View&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logic using both &lt;code&gt;MinHeap&lt;/code&gt; and &lt;code&gt;MaxHeap&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Heap View&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Word Ladder-style BFS&lt;/td&gt;
&lt;td&gt;Word Ladder View&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container / rain-water / histogram pointers&lt;/td&gt;
&lt;td&gt;Area View&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calculator character positions and sign stack&lt;/td&gt;
&lt;td&gt;Expression View&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When multiple candidate Views are found, DSA View View assigns priorities and selects a primary Visualization.&lt;/p&gt;

&lt;p&gt;For example, if heap state is available, Heap View takes priority over an ordinary array View.&lt;/p&gt;

&lt;h3&gt;
  
  
  Visualization Is Built from &lt;code&gt;ExecutionState&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Each Visualizer knows nothing about the user’s source code.&lt;/p&gt;

&lt;p&gt;It extracts what it needs from the current &lt;code&gt;ExecutionState&lt;/code&gt; and converts that data into View-specific state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BinarySearchVisualizer&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;indexState&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;currentStep&lt;/code&gt; changes, the same Visualizer receives a new state.&lt;/p&gt;

&lt;p&gt;CSS transitions and Framer Motion animate the difference, making pointers and nodes appear to move.&lt;/p&gt;

&lt;p&gt;This creates a clean separation of responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runtime creates a correct trace while preserving the original behavior&lt;/li&gt;
&lt;li&gt;Detection decides which View best represents the trace&lt;/li&gt;
&lt;li&gt;Visualization focuses on how to display that state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding a new visualization generally follows this path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detect the target execution state&lt;/li&gt;
&lt;li&gt;Convert it into View-specific state&lt;/li&gt;
&lt;li&gt;Render it with a React component&lt;/li&gt;
&lt;li&gt;Add it to the primary-visualization selection&lt;/li&gt;
&lt;li&gt;Add the smallest reproducible code sample as a test&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🧪 Tests: Reducing the Number of "It Happened to Work" Cases
&lt;/h2&gt;

&lt;p&gt;Users can write code freely, which means they can also write it in ways I never expected.&lt;/p&gt;

&lt;p&gt;AST transformations are especially dangerous because fixing one syntax pattern can quietly break another.&lt;/p&gt;

&lt;p&gt;So when a real issue appears, I keep the smallest version of that code as a regression test.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Arguments and local variables for each recursive call&lt;/li&gt;
&lt;li&gt;Postfix &lt;code&gt;i++&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;XOR assignment&lt;/li&gt;
&lt;li&gt;Nested array mutations&lt;/li&gt;
&lt;li&gt;Class constructors and &lt;code&gt;super()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TreeNode&lt;/code&gt; / &lt;code&gt;ListNode&lt;/code&gt; inputs&lt;/li&gt;
&lt;li&gt;Heap operations&lt;/li&gt;
&lt;li&gt;Binary Search and Sliding Window pointers&lt;/li&gt;
&lt;li&gt;Word Ladder BFS state&lt;/li&gt;
&lt;li&gt;Worker timeouts and step limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trusting human memory to say,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Surely this syntax still works."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;is not a particularly strong testing strategy.&lt;/p&gt;

&lt;p&gt;Memory is surprisingly optimistic.&lt;/p&gt;

&lt;p&gt;And it would be a little embarrassing to build a tool for avoiding confusion in DSA, only to get lost inside the tool’s own behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Summary
&lt;/h2&gt;

&lt;p&gt;DSA View View is not simply turning an array into a bar chart.&lt;/p&gt;

&lt;p&gt;At a high level, it works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write TypeScript in Monaco&lt;/li&gt;
&lt;li&gt;Analyze the source code and signature with Babel&lt;/li&gt;
&lt;li&gt;Inject &lt;code&gt;recordStep&lt;/code&gt; into the AST&lt;/li&gt;
&lt;li&gt;Execute the code inside a Web Worker&lt;/li&gt;
&lt;li&gt;Record variable snapshots and the call stack&lt;/li&gt;
&lt;li&gt;Replay the steps with React&lt;/li&gt;
&lt;li&gt;Display a View matching the data structure&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most important step was converting the user’s code into an array of &lt;code&gt;ExecutionStep&lt;/code&gt;s.&lt;/p&gt;

&lt;p&gt;Once execution becomes a sequence of steps, the same &lt;code&gt;ExecutionState&lt;/code&gt; can power:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pause&lt;/li&gt;
&lt;li&gt;Forward and backward navigation&lt;/li&gt;
&lt;li&gt;Timeline jumps&lt;/li&gt;
&lt;li&gt;Navigation to the previous variable change&lt;/li&gt;
&lt;li&gt;Reconstruction of recursion trees&lt;/li&gt;
&lt;li&gt;Data-structure visualizations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key idea is "Do not try to reverse JavaScript itself. First, transform execution into a recordable domain model".&lt;/p&gt;

&lt;p&gt;Then,&lt;/p&gt;

&lt;p&gt;"Runtime focuses on producing a correct trace, while Visualization focuses on presenting that trace."&lt;/p&gt;

&lt;p&gt;Keeping Editor, Runtime, and Visualization independent from one another has been essential to making the tool extensible. Stable models such as &lt;code&gt;ExecutionStep&lt;/code&gt; and &lt;code&gt;ExecutionState&lt;/code&gt; connect these layers without coupling them directly.&lt;/p&gt;

&lt;p&gt;There are still unsupported algorithms and syntax patterns, so I would love to hear from you if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An implementation does not run&lt;/li&gt;
&lt;li&gt;You want another data structure visualized&lt;/li&gt;
&lt;li&gt;You think a trace would be clearer in a different View&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Issues and PRs are always welcome 👇&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;https://github.com/nyaomaru/dsa-view-view&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now let’s run our code and watch what it is doing inside together. 👀👀&lt;/p&gt;

&lt;p&gt;And if you enjoy the project, I would be very happy if you left a GitHub ⭐!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I Built the Editor and Compile Pipeline for a TypeScript DSA Visualizer (DSA View View 👀👀)</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Wed, 22 Jul 2026 14:06:50 +0000</pubDate>
      <link>https://dev.to/nyaomaru/how-i-built-the-editor-and-compile-pipeline-for-a-typescript-dsa-visualizer-dsa-view-view--530f</link>
      <guid>https://dev.to/nyaomaru/how-i-built-the-editor-and-compile-pipeline-for-a-typescript-dsa-visualizer-dsa-view-view--530f</guid>
      <description>&lt;p&gt;Hoi hoi!&lt;/p&gt;

&lt;p&gt;I'm &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;@nyaomaru&lt;/a&gt;, a frontend engineer. Thanks to my daily Duolingo streak, Dutch is finally starting to sound slightly less like one extremely long word. Progress! 🦉&lt;/p&gt;

&lt;p&gt;In my previous article, I introduced &lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;&lt;strong&gt;DSA View View&lt;/strong&gt;&lt;/a&gt;, a tool for visualizing data structures and algorithm execution.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9vae2g6ny3lopz5jzsmy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9vae2g6ny3lopz5jzsmy.gif" alt="DSA View View fish"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/nyaomaru/i-built-a-tool-to-visualize-dsa-lets-learn-together-dsa-view-view--djo" class="crayons-story__hidden-navigation-link"&gt;I Built a Tool to Visualize DSA. Let’s Learn Together! (DSA View View 👀👀)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/nyaomaru/i-built-a-tool-to-visualize-dsa-lets-learn-together-dsa-view-view--djo" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Features a timeline to step backward mid-loop&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/nyaomaru" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1436158%2Fa78bc4ed-7796-4760-baa5-06283c2404d6.png" alt="nyaomaru profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/nyaomaru" class="crayons-story__secondary fw-medium m:hidden"&gt;
              nyaomaru
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                nyaomaru
                
              
              &lt;div id="story-author-preview-content-4143027" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/nyaomaru" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1436158%2Fa78bc4ed-7796-4760-baa5-06283c2404d6.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;nyaomaru&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/nyaomaru/i-built-a-tool-to-visualize-dsa-lets-learn-together-dsa-view-view--djo" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 15&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/nyaomaru/i-built-a-tool-to-visualize-dsa-lets-learn-together-dsa-view-view--djo" id="article-link-4143027"&gt;
          I Built a Tool to Visualize DSA. Let’s Learn Together! (DSA View View 👀👀)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/showdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;showdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/typescript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;typescript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/dsa"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;dsa&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/react"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;react&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/nyaomaru/i-built-a-tool-to-visualize-dsa-lets-learn-together-dsa-view-view--djo" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;54&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/nyaomaru/i-built-a-tool-to-visualize-dsa-lets-learn-together-dsa-view-view--djo#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              21&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            7 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;That article covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why I decided to study DSA again&lt;/li&gt;
&lt;li&gt;What DSA View View is&lt;/li&gt;
&lt;li&gt;How to use it&lt;/li&gt;
&lt;li&gt;How I plan to study with it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So if this is your first time meeting &lt;strong&gt;Mr. View&lt;/strong&gt;, the previous article is a good place to start.&lt;/p&gt;

&lt;p&gt;This time, we're opening the hood.&lt;/p&gt;

&lt;p&gt;This is the first half of the implementation story, covering everything from the moment a user writes &lt;code&gt;TypeScript&lt;/code&gt; to the moment &lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;DSA View View&lt;/a&gt; generates an input form for that implementation.&lt;/p&gt;

&lt;p&gt;We'll mainly look at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The overall architecture and FSD&lt;/li&gt;
&lt;li&gt;Runtime type guards with &lt;a href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;&lt;code&gt;is-kit&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Monaco&lt;/code&gt; Editor&lt;/li&gt;
&lt;li&gt;Static &lt;code&gt;TypeScript&lt;/code&gt; analysis&lt;/li&gt;
&lt;li&gt;Compilation with &lt;code&gt;Babel&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Extracting function signatures from the AST&lt;/li&gt;
&lt;li&gt;Generating the Verification form&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find the source code here:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;
        nyaomaru
      &lt;/a&gt; / &lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;
        dsa-view-view
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      DSA View View allows you to understand DSA to see the data flow. 👀👀 Of course, it's free.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/nyaomaru/dsa-view-view/./public/readme-view-view.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fdsa-view-view%2FHEAD%2F.%2Fpublic%2Freadme-view-view.png" alt="DSA View View" width="100%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;DSA View View&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;DSA View View turns TypeScript algorithm functions into step-by-step visual
stories. 👀👀&lt;/p&gt;
&lt;p&gt;&lt;a href="https://dsa-view-view.vercel.app/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fdsa-view-view%2FHEAD%2F.%2Fpublic%2Fdemo%2Fdsa-view-view-tv.gif" alt="DSA View View TV"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Write code, run it with structured inputs, and see the arrays, matrices
trees, lists, stacks, pointers, and return values move as the function executes.&lt;/p&gt;
&lt;p&gt;It is built for those moments when reading the code is not enough and you want
to &lt;em&gt;view&lt;/em&gt; why the answer changes.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://dsa-view-view.vercel.app/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fdsa-view-view%2FHEAD%2F.%2Fpublic%2Fdemo%2Fdsa-view-view-demo.gif" alt="DSA View View demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why Try It?&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;🧠 &lt;strong&gt;Step through real TypeScript&lt;/strong&gt;&lt;br&gt;
Paste or edit a function, validate it, then run the exact code in the browser.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🧩 &lt;strong&gt;Views that match the data&lt;/strong&gt;&lt;br&gt;
Arrays become bars, matrices become grids, trees become node graphs, linked lists become chains, and two-pointer area problems get their own visual view.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🌳 &lt;strong&gt;DSA-friendly inputs out of the box&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;TreeNode&lt;/code&gt;, &lt;code&gt;ListNode&lt;/code&gt;, &lt;code&gt;MinHeap&lt;/code&gt;, &lt;code&gt;MaxHeap&lt;/code&gt;, &lt;code&gt;PriorityQueue&lt;/code&gt;, nested arrays
matrices, strings, numbers, and class-style inputs are supported without
ceremony.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🔎 &lt;strong&gt;39 built-in examples&lt;/strong&gt;&lt;br&gt;
Search by name, browse…&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;All right, let's take a look together!&lt;/p&gt;




&lt;h2&gt;
  
  
  📝 Designing DSA View View
&lt;/h2&gt;

&lt;p&gt;DSA View View has four main responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Editor&lt;/li&gt;
&lt;li&gt;Compile&lt;/li&gt;
&lt;li&gt;Runtime&lt;/li&gt;
&lt;li&gt;Visualize&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a Verification step in the UI, where the user provides input values before the code is executed.&lt;/p&gt;

&lt;p&gt;The overall flow looks like below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwpqg19os1rtxlfqkidks.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwpqg19os1rtxlfqkidks.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each part is already a little complicated on its own, and they interact with one another quite deeply.&lt;/p&gt;

&lt;p&gt;I could see during the design phase that a flat project structure would eventually leave future-me wandering around the repository like a lost hiker. 🌲&lt;/p&gt;

&lt;p&gt;So I decided to use &lt;strong&gt;FSD (Feature-Sliced Design)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I previously wrote about how I learned FSD here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/nyaomaru/lets-learn-feature-sliced-design-fsd-15bb"&gt;https://dev.to/nyaomaru/lets-learn-feature-sliced-design-fsd-15bb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A simplified version of the project structure looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── app/                # App startup and global styles
├── pages/
│   └── main/           # Main page that composes the features
├── widgets/
│   ├── editor-panel/   # UI around the editor
│   ├── control-panel/  # Compile / Verification / Runtime controls
│   └── header/
├── features/
│   ├── code-editing/   # Monaco Editor, formatting, and parsing
│   ├── compilation/    # Compile behavior
│   ├── code-execution/ # AST transforms, execution, step recording, input forms
│   ├── visualization/  # Visualization detection and views
│   └── shareable-url/
├── entities/
│   ├── code/           # Code, types, and compile results
│   ├── execution/      # ExecutionState and ExecutionStep
│   ├── data-structure/ # TreeNode, ListNode, and friends
│   └── algorithm-example/
└── shared/
    ├── ui/             # Generic components such as Button, Card, and Dialog
    └── lib/            # Guards, cloning, serializers, and other utilities
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, &lt;code&gt;features/code-execution&lt;/code&gt; owns the behavior of executing code.&lt;/p&gt;

&lt;p&gt;On the other hand, &lt;code&gt;ExecutionState&lt;/code&gt; and &lt;code&gt;ExecutionStep&lt;/code&gt; describe domain concepts, so they live in &lt;code&gt;entities/execution&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;pages/main&lt;/code&gt; combines the individual features and coordinates the overall flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After Compile succeeds, extract the signature and move to Verification&lt;/li&gt;
&lt;li&gt;After receiving the inputs, start the Runtime&lt;/li&gt;
&lt;li&gt;When the Runtime step changes, highlight the corresponding line in the Editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each slice is consumed through a public API such as &lt;code&gt;index.ts&lt;/code&gt;, and dependencies are allowed to flow only from higher layers to lower layers.&lt;/p&gt;

&lt;p&gt;I also verify those dependency directions and public API boundaries with architecture tests.&lt;/p&gt;

&lt;p&gt;Drawing those boundaries helps prevent the classic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I changed the Runtime. Why is the Editor broken now?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A mystery nobody ordered.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛡️ &lt;code&gt;is-kit&lt;/code&gt;: Protecting the App from &lt;code&gt;unknown&lt;/code&gt; Hell
&lt;/h2&gt;

&lt;p&gt;FSD can organize dependency directions, but it cannot automatically make values entering the application safe.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;DSA View View&lt;/strong&gt;, mysterious &lt;code&gt;unknown&lt;/code&gt; values come flying in from many directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AST nodes returned by &lt;code&gt;Babel Parser&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Messages sent between &lt;code&gt;Web Worker&lt;/code&gt;s&lt;/li&gt;
&lt;li&gt;JSON restored from a shareable URL&lt;/li&gt;
&lt;li&gt;Runtime snapshots recorded from user code&lt;/li&gt;
&lt;li&gt;Arrays, trees, graphs, and heaps being inspected as visualization candidates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;TypeScript&lt;/code&gt; is wonderfully reliable at compile time.&lt;/p&gt;

&lt;p&gt;But at runtime, it does not interrogate a value and ask.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Are you &lt;em&gt;really&lt;/em&gt; an &lt;code&gt;ExecutionState&lt;/code&gt;, or are you just wearing its name tag?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is where my type guard library, &lt;a href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;&lt;code&gt;is-kit&lt;/code&gt;&lt;/a&gt;, comes in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;&lt;code&gt;is-kit&lt;/code&gt;&lt;/a&gt; is a small, zero-dependency library for building and composing reusable &lt;code&gt;TypeScript&lt;/code&gt; type guards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;and&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;arrayOf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;define&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;hasKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;oneOfValues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;or&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;predicateToRefine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Guard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is-kit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isNumericString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;predicateToRefine&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isNonArrayObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isNumericValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isNumericString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isNumericArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;arrayOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isNumericValue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small guards can be combined like LEGO bricks to create more complicated runtime checks.&lt;/p&gt;

&lt;p&gt;For example, the code that finds class methods in an AST defines a guard.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isMethodNodeType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;oneOfValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ClassMethod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MethodDefinition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hasKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isMethodLikeNode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Guard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MethodLikeNode&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;isObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;define&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MethodLikeNode&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;hasType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;isMethodNodeType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a value passes this guard, it is narrowed from a plain &lt;code&gt;unknown&lt;/code&gt; to &lt;code&gt;MethodLikeNode&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The guard can then be passed directly to &lt;code&gt;filter&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;classNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isMethodLikeNode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is extremely convenient.&lt;/p&gt;

&lt;p&gt;Without &lt;a href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;&lt;code&gt;is-kit&lt;/code&gt;&lt;/a&gt;, checks like this would have multiplied across the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eventually, slightly different versions of the same guard would appear everywhere.&lt;/p&gt;

&lt;p&gt;Six months later, I would be asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What is the difference between this &lt;code&gt;isObject&lt;/code&gt; and that &lt;code&gt;isObject&lt;/code&gt;?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And congratulations—we would have built a second maze inside the first maze.&lt;/p&gt;

&lt;p&gt;At the time of writing, shared guards are used from around &lt;strong&gt;69 files&lt;/strong&gt; under &lt;code&gt;src&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;Monaco&lt;/code&gt; and &lt;code&gt;Babel&lt;/code&gt; are the stars on stage, &lt;code&gt;is-kit&lt;/code&gt; is the very serious bouncer standing at every entrance to the AST, Worker, share URL, Runtime, and Visualization layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mr. Unknown&lt;/strong&gt; is not getting in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ynmx4d0fqp4mpny8zga.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ynmx4d0fqp4mpny8zga.png" alt="Mr. Unknown insect damage"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also use &lt;code&gt;Zod&lt;/code&gt; in the Verification forms, but the responsibilities are different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;is-kit&lt;/code&gt;: Internal narrowing, filtering, and reusable runtime guards&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Zod&lt;/code&gt;: Input form schemas, validation messages, and user-facing errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of making one library solve every problem, &lt;code&gt;is-kit&lt;/code&gt; handles internal runtime safety while &lt;code&gt;Zod&lt;/code&gt; handles user input validation.&lt;/p&gt;

&lt;p&gt;I am very glad I built &lt;code&gt;is-kit&lt;/code&gt; before building this application. Fighting all those &lt;code&gt;unknown&lt;/code&gt; values with my bare hands would have made &lt;em&gt;me&lt;/em&gt; the first thing that needed visualizing.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✏️ Editor
&lt;/h2&gt;

&lt;p&gt;The Editor uses &lt;a href="https://microsoft.github.io/monaco-editor/" rel="noopener noreferrer"&gt;Monaco Editor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Monaco is the browser-focused editor component also used by &lt;code&gt;VS Code&lt;/code&gt;, giving the application syntax highlighting, completion, and &lt;code&gt;TypeScript&lt;/code&gt; diagnostics.&lt;/p&gt;

&lt;p&gt;The downside is that Monaco is not exactly tiny.&lt;/p&gt;

&lt;p&gt;Loading everything immediately made the initial display heavier, so DSA View View first renders a lightweight &lt;code&gt;textarea&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same &lt;code&gt;textarea&lt;/code&gt; is also used as the &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; fallback, preventing the editor area from becoming empty while Monaco is loading.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;textarea&lt;/span&gt;
  &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Code Editor"&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onFocus&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onActivate&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind the scenes, DSA View View waits briefly and prepares Monaco with &lt;code&gt;requestIdleCallback&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the browser does not support &lt;code&gt;requestIdleCallback&lt;/code&gt;, loading starts normally. If the user focuses the &lt;code&gt;textarea&lt;/code&gt; before that, Monaco preparation begins immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CodeEditor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;prepareCodeEditorModule&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kr"&gt;module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CodeEditor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once loading finishes, the lightweight editor is replaced by Monaco while preserving the same source code.&lt;/p&gt;

&lt;p&gt;It looks like an editor, but secretly it is still a &lt;code&gt;textarea&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A tiny editor body-double strategy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Static &lt;code&gt;TypeScript&lt;/code&gt; Analysis
&lt;/h3&gt;

&lt;p&gt;Static analysis uses the &lt;code&gt;TypeScript Worker&lt;/code&gt; included with Monaco.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;TypeScript Language Service&lt;/code&gt; runs in a separate Worker from the editor itself and returns type and syntax problems as markers.&lt;/p&gt;

&lt;p&gt;Those markers are converted into the application's &lt;code&gt;CompilationError&lt;/code&gt; model and used to control the Compile button and display errors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CompilationError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Compile button is disabled only when an &lt;code&gt;error&lt;/code&gt; exists. Warnings alone do not prevent compilation.&lt;/p&gt;

&lt;p&gt;One important detail is that &lt;strong&gt;&lt;code&gt;Babel&lt;/code&gt; does not perform &lt;code&gt;TypeScript&lt;/code&gt; type checking&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The responsibilities are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monaco's &lt;code&gt;TypeScript&lt;/code&gt; Worker: Diagnose types and syntax&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Babel&lt;/code&gt;: Transform &lt;code&gt;TypeScript&lt;/code&gt; syntax into &lt;code&gt;JavaScript&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping that distinction clear saved me from blaming Babel for crimes it did not commit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Registering DSA Classes in the Editor
&lt;/h3&gt;

&lt;p&gt;Many &lt;code&gt;LeetCode&lt;/code&gt; problems provide classes such as &lt;code&gt;TreeNode&lt;/code&gt; and &lt;code&gt;ListNode&lt;/code&gt; automatically.&lt;/p&gt;

&lt;p&gt;DSA View View also provides common classes so users do not have to declare them every time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TreeNode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ListNode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GraphNode&lt;/code&gt; / &lt;code&gt;_Node&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TrieNode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PriorityQueue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MinHeap&lt;/code&gt; / &lt;code&gt;MaxHeap&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Deque&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UnionFind&lt;/code&gt; / &lt;code&gt;DSU&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Counter&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Monaco provides a mechanism called &lt;code&gt;setExtraLibs&lt;/code&gt;, which lets the application add these type definitions to the Editor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;monaco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;typescript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;typescriptDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setExtraLibs&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ts:algorithm-visualizer-prepared-classes.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, &lt;code&gt;setExtraLibs&lt;/code&gt; only supports type analysis inside the Editor. It does not magically create those classes at Runtime.&lt;/p&gt;

&lt;p&gt;So the Compile and Runtime pipelines also prepend the same class definitions to the user's code as a prelude.&lt;/p&gt;

&lt;p&gt;If the user declares a &lt;code&gt;class&lt;/code&gt;, &lt;code&gt;interface&lt;/code&gt;, or &lt;code&gt;type&lt;/code&gt; with the same name, DSA View View skips injecting the prepared definition.&lt;/p&gt;

&lt;p&gt;Imagine carefully writing your own &lt;code&gt;ListNode&lt;/code&gt;, only for another &lt;code&gt;ListNode&lt;/code&gt; to emerge from behind it.&lt;/p&gt;

&lt;p&gt;That is not type safety. That is a summer horror movie.&lt;/p&gt;

&lt;h3&gt;
  
  
  Formatting
&lt;/h3&gt;

&lt;p&gt;Formatting uses &lt;code&gt;Prettier&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Prettier core, &lt;code&gt;TypeScript&lt;/code&gt; plugin, and &lt;code&gt;Estree&lt;/code&gt; plugin are dynamically imported only when needed and registered as Monaco's &lt;code&gt;Document Formatting Provider&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Users can therefore format their code with &lt;code&gt;Command/Ctrl + S&lt;/code&gt; or &lt;code&gt;Shift + Alt + F&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  📌 From the Compile Button to Verification
&lt;/h2&gt;

&lt;p&gt;So far, I have described "Compile" as if it were one large operation.&lt;/p&gt;

&lt;p&gt;Internally, however, the FSD slice &lt;code&gt;features/compilation&lt;/code&gt; does not own the whole flow.&lt;/p&gt;

&lt;p&gt;When the user presses the Compile button, &lt;code&gt;pages/main&lt;/code&gt; coordinates these operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;features/compilation&lt;/code&gt; checks whether the &lt;code&gt;TypeScript&lt;/code&gt; can be transformed into &lt;code&gt;JavaScript&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The parser in &lt;code&gt;features/code-editing&lt;/code&gt; extracts the function or class signature&lt;/li&gt;
&lt;li&gt;If everything succeeds, the UI moves to Verification&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;features/code-execution&lt;/code&gt; displays an input form matching that signature&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To the user, this feels like one continuous Compile step.&lt;/p&gt;

&lt;p&gt;Inside the application, each responsibility remains separate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transforming &lt;code&gt;TypeScript&lt;/code&gt; with Babel
&lt;/h3&gt;

&lt;p&gt;The application uses Babel to transform &lt;code&gt;TypeScript&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prepared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;prepareTypeScriptCodeWithClasses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prepared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;presets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;typescript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DSA View View runs entirely in the browser, so it is not invoking &lt;code&gt;tsc&lt;/code&gt; on a &lt;code&gt;Node.js&lt;/code&gt; server.&lt;/p&gt;

&lt;p&gt;Instead, it uses &lt;code&gt;@babel/standalone&lt;/code&gt; to transform &lt;code&gt;TypeScript&lt;/code&gt; syntax into &lt;code&gt;JavaScript&lt;/code&gt; in the browser.&lt;/p&gt;

&lt;p&gt;Before transformation, the application prepends any prepared classes that the user has not declared themselves.&lt;/p&gt;

&lt;p&gt;If the source contains imports for those prepared classes, they are removed because the browser execution environment cannot resolve those modules.&lt;/p&gt;

&lt;p&gt;Again, an important reminder:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Babel's &lt;code&gt;TypeScript&lt;/code&gt; preset primarily removes type information. It does not type-check the program.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Monaco handles type errors. Babel checks whether the syntax can be transformed.&lt;/p&gt;

&lt;p&gt;The generated &lt;code&gt;JavaScript&lt;/code&gt; from this stage is also not executed directly on the main thread.&lt;/p&gt;

&lt;p&gt;For the Runtime, the original source is instrumented separately and then executed inside a &lt;code&gt;Web Worker&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is the fun part—and the topic of the next article.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extracting Function Signatures from the AST
&lt;/h3&gt;

&lt;p&gt;After Compile succeeds, DSA View View needs to build an input form.&lt;/p&gt;

&lt;p&gt;Suppose the user writes this function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;trap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source code is parsed into an AST with &lt;code&gt;Babel Parser&lt;/code&gt;, and DSA View View extracts information like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trap&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;height&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number-array&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;returnType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser supports more than ordinary function declarations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Function declarations&lt;/li&gt;
&lt;li&gt;Arrow functions assigned to variables&lt;/li&gt;
&lt;li&gt;Function expressions assigned to variables&lt;/li&gt;
&lt;li&gt;LeetCode-style factory functions that return another function&lt;/li&gt;
&lt;li&gt;Class declarations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a class, the parser extracts the constructor and callable methods. &lt;code&gt;private&lt;/code&gt; methods are currently excluded.&lt;/p&gt;

&lt;p&gt;For class-design problems such as &lt;code&gt;LRUCache&lt;/code&gt; and &lt;code&gt;MedianFinder&lt;/code&gt;, it extracts the class name, constructor, method names, and arguments.&lt;/p&gt;

&lt;p&gt;When the source contains multiple helper classes, DSA View View avoids prioritizing prepared classes as solution candidates and compares the number of callable methods to select the likely design class.&lt;/p&gt;

&lt;p&gt;This selection is heuristic, so it cannot perfectly identify every possible class structure yet.&lt;/p&gt;

&lt;p&gt;There are always more creative ways to write JavaScript than I expect. JavaScript considers this a feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generating the Verification Form
&lt;/h3&gt;

&lt;p&gt;For ordinary functions, the input form is built from the extracted signature using &lt;code&gt;React Hook Form&lt;/code&gt; and &lt;code&gt;Zod&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Supported input types include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;number&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;string&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;number[]&lt;/code&gt; / &lt;code&gt;string[]&lt;/code&gt; / &lt;code&gt;boolean[]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;number[][]&lt;/code&gt; / &lt;code&gt;string[][]&lt;/code&gt; / &lt;code&gt;boolean[][]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TreeNode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ListNode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GraphNode&lt;/code&gt; / &lt;code&gt;_Node&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The input UI and validation behavior change depending on the type.&lt;/p&gt;

&lt;p&gt;For example, a &lt;code&gt;number[]&lt;/code&gt; accepts both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1, 2, 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[1, 2, 3]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both formats are converted into a real array that can be passed to the Runtime.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;TreeNode&lt;/code&gt;, the application constructs a tree from a level-order array.&lt;/p&gt;

&lt;p&gt;When a problem has multiple &lt;code&gt;TreeNode&lt;/code&gt; parameters, users can also select a specific node from the first root.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;ListNode&lt;/code&gt;, users can build a linked list from an array or node-based input and specify the cycle position.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;GraphNode&lt;/code&gt;, a cyclic graph structure is constructed from a &lt;code&gt;LeetCode&lt;/code&gt;-style adjacency list.&lt;/p&gt;

&lt;p&gt;Class-design problems use a different UI.&lt;/p&gt;

&lt;p&gt;Instead of the normal parameter form powered by &lt;code&gt;React Hook Form&lt;/code&gt; and &lt;code&gt;Zod&lt;/code&gt;, DSA View View displays the same &lt;code&gt;Operations&lt;/code&gt; and &lt;code&gt;Arguments&lt;/code&gt; JSON format used by &lt;code&gt;LeetCode&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;MedianFinder&lt;/code&gt; uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Operations: ["MedianFinder", "addNum", "addNum", "findMedian"]
Arguments:  [[], [1], [2], []]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This form validates class-design-specific requirements, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The number of operations matches the number of argument lists&lt;/li&gt;
&lt;li&gt;The first operation matches the class name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the Compile button starts a chain of separate responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Babel verifies that transformation is possible&lt;/li&gt;
&lt;li&gt;AST analysis extracts the signature&lt;/li&gt;
&lt;li&gt;The Verification layer generates the appropriate input form&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pages/main&lt;/code&gt; connects the entire flow from the Editor to Verification&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎯 Summary
&lt;/h2&gt;

&lt;p&gt;In this article, we followed the path from writing &lt;code&gt;TypeScript&lt;/code&gt; in DSA View View to generating the inputs needed for execution.&lt;/p&gt;

&lt;p&gt;The broad flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Render a lightweight &lt;code&gt;textarea&lt;/code&gt; first&lt;/li&gt;
&lt;li&gt;Prepare Monaco Editor in the background&lt;/li&gt;
&lt;li&gt;Diagnose types and syntax with the &lt;code&gt;TypeScript&lt;/code&gt; Worker&lt;/li&gt;
&lt;li&gt;Use Babel to check whether the &lt;code&gt;TypeScript&lt;/code&gt; can be transformed into &lt;code&gt;JavaScript&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Extract the function or class signature from the AST&lt;/li&gt;
&lt;li&gt;Generate a Verification form based on the detected types&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important design decision was not forcing the Editor, diagnostics, syntax transformation, and input UI into one giant operation.&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;&lt;code&gt;is-kit&lt;/code&gt;&lt;/a&gt; safely narrows &lt;code&gt;unknown&lt;/code&gt; values from the AST, Workers, JSON, and runtime snapshots&lt;/li&gt;
&lt;li&gt;Monaco's &lt;code&gt;TypeScript&lt;/code&gt; Worker diagnoses types and syntax&lt;/li&gt;
&lt;li&gt;Babel transforms &lt;code&gt;TypeScript&lt;/code&gt; syntax into &lt;code&gt;JavaScript&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;AST analysis extracts the signature needed by the Verification UI&lt;/li&gt;
&lt;li&gt;The Verification form validates inputs and converts them into Runtime values&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pages/main&lt;/code&gt; connects the separate steps and advances the UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping those responsibilities separate made the flow much easier to understand and extend.&lt;/p&gt;

&lt;p&gt;In the next article, we will finally enter the heart of DSA View View: the Runtime.&lt;/p&gt;

&lt;p&gt;That is where &lt;strong&gt;Mr. View&lt;/strong&gt; stops politely waiting beside the Editor and starts watching every move your variables make. 👀&lt;/p&gt;

&lt;p&gt;There are still unsupported patterns and types, so Issues and PRs are always welcome!&lt;/p&gt;

&lt;p&gt;And if you enjoy the project, I would be very happy if you left a GitHub ⭐!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;https://github.com/nyaomaru/dsa-view-view&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>react</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Built a Tool to Visualize DSA. Let’s Learn Together! (DSA View View 👀👀)</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Wed, 15 Jul 2026 12:04:25 +0000</pubDate>
      <link>https://dev.to/nyaomaru/i-built-a-tool-to-visualize-dsa-lets-learn-together-dsa-view-view--djo</link>
      <guid>https://dev.to/nyaomaru/i-built-a-tool-to-visualize-dsa-lets-learn-together-dsa-view-view--djo</guid>
      <description>&lt;p&gt;Hoi hoi!&lt;/p&gt;

&lt;p&gt;I’m &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;@nyaomaru&lt;/a&gt;, a frontend engineer currently fighting the intense European heatwave by building DIY window screens here in the Netherlands. 🛠️&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/data-science-collective/should-you-still-learn-to-code-in-2026-034685e17707" rel="noopener noreferrer"&gt;https://medium.com/data-science-collective/should-you-still-learn-to-code-in-2026-034685e17707&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As this article also discusses, I believe that &lt;strong&gt;improving our fundamental engineering skills is still essential in the age of AI&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Imagine that AI makes an F1 car fully autonomous. 🏎️&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Would you let someone without even a driver’s license enter a race as its driver?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or imagine that AI completely automates maintenance during pit stops. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Would you put someone who knows absolutely nothing about cars, their parts, or their internal structure in charge of that AI?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI is certainly reducing the amount of code we need to write by hand. Typing quickly and memorizing every small piece of syntax or every method are becoming less important.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;can we confidently release a product without understanding its fundamental concepts or implementation?&lt;/strong&gt; If something goes wrong in production, can we take responsibility and fix it?&lt;/p&gt;

&lt;p&gt;My answer was &lt;strong&gt;no&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Building things faster is obviously a good thing. But if we build without understanding, we may simply produce fragile software with no long-term sustainability at a faster rate.&lt;/p&gt;

&lt;p&gt;The real value comes from building meaningful things quickly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Then, how can we develop the fundamental skills we still need in the AI era?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I believe one useful approach is learning &lt;strong&gt;DSA: Data Structures and Algorithms&lt;/strong&gt;. So I decided to start learning DSA again.&lt;/p&gt;

&lt;p&gt;However, while solving problems on &lt;code&gt;LeetCode&lt;/code&gt;, I repeatedly ran into the same problem.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I want to visualize the data flow and understand what is happening, but I still can’t clearly see it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I used the skills I have developed through frontend engineering and built a tool that visualizes the execution flow of DSA implementations! 🚀&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;
        nyaomaru
      &lt;/a&gt; / &lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;
        dsa-view-view
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      DSA View View allows you to understand DSA to see the data flow. 👀👀 Of course, it's free.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/nyaomaru/dsa-view-view/./public/readme-view-view.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fdsa-view-view%2FHEAD%2F.%2Fpublic%2Freadme-view-view.png" alt="DSA View View" width="100%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;DSA View View&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;DSA View View turns TypeScript algorithm functions into step-by-step visual
stories. 👀👀&lt;/p&gt;
&lt;p&gt;&lt;a href="https://dsa-view-view.vercel.app/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fdsa-view-view%2FHEAD%2F.%2Fpublic%2Fdemo%2Fdsa-view-view-tv.gif" alt="DSA View View TV"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Write code, run it with structured inputs, and see the arrays, matrices
trees, lists, stacks, pointers, and return values move as the function executes.&lt;/p&gt;
&lt;p&gt;It is built for those moments when reading the code is not enough and you want
to &lt;em&gt;view&lt;/em&gt; why the answer changes.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://dsa-view-view.vercel.app/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fdsa-view-view%2FHEAD%2F.%2Fpublic%2Fdemo%2Fdsa-view-view-demo.gif" alt="DSA View View demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why Try It?&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;🧠 &lt;strong&gt;Step through real TypeScript&lt;/strong&gt;&lt;br&gt;
Paste or edit a function, validate it, then run the exact code in the browser.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🧩 &lt;strong&gt;Views that match the data&lt;/strong&gt;&lt;br&gt;
Arrays become bars, matrices become grids, trees become node graphs, linked lists become chains, and two-pointer area problems get their own visual view.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🌳 &lt;strong&gt;DSA-friendly inputs out of the box&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;TreeNode&lt;/code&gt;, &lt;code&gt;ListNode&lt;/code&gt;, nested arrays, matrices, strings, numbers, and class
style inputs are supported without ceremony.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🔎 &lt;strong&gt;39 built-in examples&lt;/strong&gt;&lt;br&gt;
Search by name, browse by category, and jump into…&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flt3slpuvq0bt9yzzjtkm.gif" alt="DSA View View demo" width="600" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, I’ll explain how &lt;strong&gt;DSA View View&lt;/strong&gt; works and how we can use it to study DSA.&lt;/p&gt;

&lt;p&gt;Let’s take a look together!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;&lt;br&gt;
I may sound confident so far, but I’m definitely not a DSA expert. I’m still learning, just like many of you.&lt;/p&gt;

&lt;p&gt;In fact, I built this tool precisely because I was struggling. When I realized that I couldn’t immediately solve Binary Search, I started to panic a little. 😸&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🖥️ What Is DSA View View?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;DSA View View&lt;/strong&gt; is a tool that executes functions and classes written in &lt;code&gt;TypeScript&lt;/code&gt; and lets you inspect how variables and return values change on each line through a runtime timeline.&lt;/p&gt;

&lt;p&gt;The easiest way to understand is to try it yourself!&lt;/p&gt;

&lt;p&gt;Open the tool and click the &lt;strong&gt;Run Demo&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frdur3bu1s24luupivhly.png" alt="Run Demo Button" width="798" height="141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You’ll quickly see how the workflow works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write your implementation&lt;/li&gt;
&lt;li&gt;Compile it&lt;/li&gt;
&lt;li&gt;Enter the input values&lt;/li&gt;
&lt;li&gt;Run it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s really it.&lt;/p&gt;

&lt;p&gt;Although I created it primarily for DSA problems, you can also use it to inspect and test smaller &lt;code&gt;TypeScript&lt;/code&gt; implementations.&lt;/p&gt;

&lt;p&gt;And, of course, it’s completely free!&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 What Can It Do?
&lt;/h2&gt;

&lt;p&gt;With DSA View View, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write DSA implementations in &lt;code&gt;TypeScript&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Test implementations with different input values&lt;/li&gt;
&lt;li&gt;Move forward and backward through runtime steps&lt;/li&gt;
&lt;li&gt;Visualize supported data structures and algorithms&lt;/li&gt;
&lt;li&gt;Review LeetCode solutions and confirm your understanding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also use it as a lightweight environment for executing functions or formatting code blocks for technical articles such as Dev.to 😸&lt;/p&gt;

&lt;p&gt;Feel free to experiment with it and find your own use cases!&lt;/p&gt;




&lt;h2&gt;
  
  
  🧑‍🏫 How to Use It
&lt;/h2&gt;

&lt;p&gt;The main workflow consists of three modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Editor&lt;/li&gt;
&lt;li&gt;Verification&lt;/li&gt;
&lt;li&gt;Runtime&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s look at each one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Editor
&lt;/h2&gt;

&lt;p&gt;First, write your implementation!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg96n2pdp7atrfm0i9y9k.png" alt=" " width="799" height="605"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Editor is designed to feel similar to a basic IDE.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TypeScript&lt;/code&gt; diagnostics are available by default, so the editor can detect some errors while you write.&lt;/p&gt;

&lt;p&gt;You can also press &lt;code&gt;Cmd/Ctrl + S&lt;/code&gt; to format your code with Prettier, so feel free to write first and clean it up afterward.&lt;/p&gt;

&lt;p&gt;Common classes used in DSA problems, such as &lt;code&gt;TreeNode&lt;/code&gt; and &lt;code&gt;ListNode&lt;/code&gt;, are already available.&lt;/p&gt;

&lt;p&gt;You don’t need to declare or import them every time.&lt;/p&gt;

&lt;p&gt;You can also define your own classes or replace the provided definitions when necessary.&lt;/p&gt;

&lt;p&gt;Once your implementation is ready, click &lt;strong&gt;Compile Code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If compilation fails, the tool displays the relevant location and error message so that you can fix it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fss7ylft3mi1q5ur2sw1z.png" alt=" " width="800" height="752"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is also an &lt;strong&gt;Example&lt;/strong&gt; menu containing several common implementations.&lt;/p&gt;

&lt;p&gt;For example, if you want to quickly inspect a Heap Sort implementation and see how it behaves, you can load the example instead of writing everything manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;In Verification mode, you define the input values that should be passed to your function or class.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhz024r1f92790yj7bkrf.png" alt=" " width="800" height="751"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enter a test value for each argument.&lt;/p&gt;

&lt;p&gt;For arrays, both of the following formats are supported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[1, 2, 3]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1, 2, 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once your input values are ready, click &lt;strong&gt;Run&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Runtime
&lt;/h2&gt;

&lt;p&gt;Finally, let’s inspect the runtime!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7bziyhlkont1v3m2odh7.png" alt=" " width="800" height="755"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you click &lt;strong&gt;Run&lt;/strong&gt; in Verification mode, your implementation is executed.&lt;/p&gt;

&lt;p&gt;Runtime mode then lets you play through the execution steps or move backward and forward manually.&lt;/p&gt;

&lt;p&gt;When the tool recognizes supported logic, &lt;strong&gt;an additional visualization is displayed in a modal&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not every possible implementation is supported yet, of course. If something you need is missing, I would be happy to receive an issue or pull request!&lt;/p&gt;

&lt;p&gt;You can move backward and forward through the executed lines to inspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which line is currently running&lt;/li&gt;
&lt;li&gt;How variables change&lt;/li&gt;
&lt;li&gt;Where values are returned&lt;/li&gt;
&lt;li&gt;How the execution reaches its final result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can return to the beginning, jump directly to the end, or move through the timeline one step at a time.&lt;/p&gt;

&lt;p&gt;The main execution process runs entirely in the browser.&lt;/p&gt;


&lt;h2&gt;
  
  
  Share 🐈
&lt;/h2&gt;

&lt;p&gt;DSA View View also includes a sharing feature.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvzdkh18relxhb1sfir94.png" alt="DSA View View Share button" width="800" height="213"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can create a URL that preserves your implementation and share it with someone else.&lt;/p&gt;

&lt;p&gt;Use the &lt;strong&gt;Share&lt;/strong&gt; button at the top of the page to post it on social media or copy the link directly.&lt;/p&gt;

&lt;p&gt;Share your implementations, compare different solutions, and let’s learn together!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; The shared URL contains your implementation. Do not paste private, confidential, or proprietary code into the tool before sharing it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  What Is That Thing in the Top-Right Corner? 👀👀
&lt;/h2&gt;

&lt;p&gt;Meet Mr. View.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4k6eygcli1kzrkr62qn7.gif" alt=" " width="510" height="161"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Mr. View watches over your implementation.&lt;/p&gt;

&lt;p&gt;Nothing more.&lt;/p&gt;

&lt;p&gt;Nothing less.&lt;/p&gt;

&lt;p&gt;He is simply there to support you.&lt;/p&gt;

&lt;p&gt;When you need to concentrate, press the TV button to hide him. Press it again to bring him back.&lt;/p&gt;

&lt;p&gt;However, if Mr. View is enough to distract you, you may not be ready for an attack from ten simultaneous Slack notifications. 😸&lt;/p&gt;


&lt;h2&gt;
  
  
  ✏️ How Should We Study DSA?
&lt;/h2&gt;

&lt;p&gt;Now you know how to use DSA View View.&lt;/p&gt;

&lt;p&gt;The next question is probably&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which problems should I solve?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;LeetCode is an obvious place to start, and the study plan from Tech Interview Handbook also provides a useful collection of practice problems. 👇&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.techinterviewhandbook.org/coding-interview-study-plan/" rel="noopener noreferrer"&gt;https://www.techinterviewhandbook.org/coding-interview-study-plan/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try solving these problems with &lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;DSA View View&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once your implementation works, use the runtime timeline to inspect the data flow and understand exactly how the algorithm reaches its answer.&lt;/p&gt;

&lt;p&gt;You can also use tools such as ChatGPT, Codex, Claude, Claude Code, Gemini, and Grok to explore your solution further.&lt;/p&gt;

&lt;p&gt;Don’t limit yourself to asking&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this implementation correct?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Try asking questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What would happen if I rewrote this DFS solution using BFS?&lt;/li&gt;
&lt;li&gt;Could Dynamic Programming solve this problem?&lt;/li&gt;
&lt;li&gt;Are there any other reasonable approaches?&lt;/li&gt;
&lt;li&gt;What are the time and space complexities?&lt;/li&gt;
&lt;li&gt;Why is this approach better than the alternatives?&lt;/li&gt;
&lt;li&gt;Under which conditions would another approach be better?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI becomes much more useful for learning when it helps us compare and understand different approaches instead of only generating a final answer. 👍&lt;/p&gt;
&lt;h3&gt;
  
  
  Why TypeScript Instead of Python? 🐍
&lt;/h3&gt;

&lt;p&gt;The simple answer is that &lt;code&gt;TypeScript&lt;/code&gt; is my favorite language.&lt;/p&gt;

&lt;p&gt;I mainly work as a frontend engineer, so &lt;code&gt;TypeScript&lt;/code&gt; is also the language in which I feel most comfortable experimenting and learning.&lt;/p&gt;

&lt;p&gt;However, I designed the tool so that additional languages could be supported in the future.&lt;/p&gt;

&lt;p&gt;If there is enough demand for another language, please leave a comment or open an issue!&lt;/p&gt;


&lt;h2&gt;
  
  
  🎯 Conclusion
&lt;/h2&gt;

&lt;p&gt;AI is helping us write code faster and faster.&lt;/p&gt;

&lt;p&gt;However, understanding what happens inside an implementation remains an important engineering skill.&lt;/p&gt;

&lt;p&gt;In fact, I believe that this ability may become even more valuable as AI-generated code becomes increasingly common.&lt;/p&gt;

&lt;p&gt;I built DSA View View because I wanted a tool that could help me strengthen those fundamental skills from the ground up.&lt;/p&gt;

&lt;p&gt;To be honest, I still have a lot to learn about DSA.&lt;/p&gt;

&lt;p&gt;That is exactly why I hope we can learn together.&lt;/p&gt;

&lt;p&gt;Please leave a comment or open an issue if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want support for a particular problem or data structure&lt;/li&gt;
&lt;li&gt;You have an idea for a new visualization&lt;/li&gt;
&lt;li&gt;You find a bug&lt;/li&gt;
&lt;li&gt;You want support for another programming language&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s train our DSA muscles together! 💪😸&lt;/p&gt;

&lt;p&gt;If you like it, please give a star ⭐&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;
        nyaomaru
      &lt;/a&gt; / &lt;a href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;
        dsa-view-view
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      DSA View View allows you to understand DSA to see the data flow. 👀👀 Of course, it's free.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/nyaomaru/dsa-view-view/./public/readme-view-view.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fdsa-view-view%2FHEAD%2F.%2Fpublic%2Freadme-view-view.png" alt="DSA View View" width="100%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;DSA View View&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;DSA View View turns TypeScript algorithm functions into step-by-step visual
stories. 👀👀&lt;/p&gt;
&lt;p&gt;&lt;a href="https://dsa-view-view.vercel.app/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fdsa-view-view%2FHEAD%2F.%2Fpublic%2Fdemo%2Fdsa-view-view-tv.gif" alt="DSA View View TV"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Write code, run it with structured inputs, and see the arrays, matrices
trees, lists, stacks, pointers, and return values move as the function executes.&lt;/p&gt;
&lt;p&gt;It is built for those moments when reading the code is not enough and you want
to &lt;em&gt;view&lt;/em&gt; why the answer changes.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://dsa-view-view.vercel.app/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fdsa-view-view%2FHEAD%2F.%2Fpublic%2Fdemo%2Fdsa-view-view-demo.gif" alt="DSA View View demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why Try It?&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;🧠 &lt;strong&gt;Step through real TypeScript&lt;/strong&gt;&lt;br&gt;
Paste or edit a function, validate it, then run the exact code in the browser.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🧩 &lt;strong&gt;Views that match the data&lt;/strong&gt;&lt;br&gt;
Arrays become bars, matrices become grids, trees become node graphs, linked lists become chains, and two-pointer area problems get their own visual view.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🌳 &lt;strong&gt;DSA-friendly inputs out of the box&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;TreeNode&lt;/code&gt;, &lt;code&gt;ListNode&lt;/code&gt;, nested arrays, matrices, strings, numbers, and class
style inputs are supported without ceremony.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🔎 &lt;strong&gt;39 built-in examples&lt;/strong&gt;&lt;br&gt;
Search by name, browse by category, and jump into…&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nyaomaru/dsa-view-view" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;And I also launched on ProductHunt! Come say hello and let me know what you think 😸 👇&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://www.producthunt.com/products/dsa-view-view" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;producthunt.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>showdev</category>
      <category>typescript</category>
      <category>dsa</category>
      <category>react</category>
    </item>
    <item>
      <title>Trying Declarative Partial Updates: A Future API for Replacing HTML Later</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Wed, 01 Jul 2026 12:42:05 +0000</pubDate>
      <link>https://dev.to/nyaomaru/trying-declarative-partial-updates-a-future-api-for-replacing-html-later-2g43</link>
      <guid>https://dev.to/nyaomaru/trying-declarative-partial-updates-a-future-api-for-replacing-html-later-2g43</guid>
      <description>&lt;p&gt;Hoi hoi!&lt;/p&gt;

&lt;p&gt;I'm &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;@nyaomaru&lt;/a&gt;, a frontend engineer who was cheering for the World Cup during tropical nights in the Netherlands while nearly melting from heat exhaustion. 🫠⚽ (But yesterday was comfortable 😺)&lt;/p&gt;

&lt;p&gt;How do you usually build frontend applications?&lt;/p&gt;

&lt;p&gt;Do you build SPAs with &lt;code&gt;React&lt;/code&gt; / &lt;code&gt;Vue&lt;/code&gt; / &lt;code&gt;Svelte&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Do you use SSR / SSG / ISR frameworks like &lt;code&gt;Next&lt;/code&gt; / &lt;code&gt;Remix&lt;/code&gt; / &lt;code&gt;Nuxt&lt;/code&gt; / &lt;code&gt;Astro&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;There are many ways to build frontend applications, but today it is very common to update the DOM on the client side using JavaScript.&lt;/p&gt;

&lt;p&gt;Even when using UI libraries and frameworks like &lt;code&gt;React&lt;/code&gt; / &lt;code&gt;Vue&lt;/code&gt; / &lt;code&gt;Svelte&lt;/code&gt;, or SSR frameworks like &lt;code&gt;Next&lt;/code&gt; / &lt;code&gt;Remix&lt;/code&gt; / &lt;code&gt;Nuxt&lt;/code&gt; / &lt;code&gt;Astro&lt;/code&gt;, there are still many cases where part of the screen is eventually updated by client-side JavaScript.&lt;/p&gt;

&lt;p&gt;But is that really always the best answer?&lt;/p&gt;

&lt;p&gt;Are there cases where we are forcing JavaScript on the client to handle work that could naturally be handled on the server?&lt;/p&gt;

&lt;p&gt;In response to that kind of question, the Chrome team introduced an experimental API called DPU, or Declarative Partial Updates. 🚀&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://developer.chrome.com/blog/declarative-partial-updates?hl=en" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdeveloper.chrome.com%2Fstatic%2Fblog%2Fdeclarative-partial-updates%2Fimage%2Fthumbnail.png" height="450" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://developer.chrome.com/blog/declarative-partial-updates?hl=en" rel="noopener noreferrer" class="c-link"&gt;
            Declarative partial updates &amp;nbsp;|&amp;nbsp; Blog &amp;nbsp;|&amp;nbsp; Chrome for Developers
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Learn about new out-of-order streaming capabilities and the renewed HTML insertion and streaming methods available for testing from Chrome 148
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.gstatic.com%2Fdevrel-devsite%2Fprod%2Fv11431966d26d9f049ef61662c2b798f1cdee8af320f1ba0f77a43eee64301d60%2Fchrome%2Fimages%2Ffavicon.png" width="32" height="32"&gt;
          developer.chrome.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;In this article, I want to explore what Declarative Partial Updates are, what problems they try to solve, and why they might matter.&lt;/p&gt;

&lt;p&gt;I will focus especially on the out-of-order streaming mechanism that can replace parts of HTML later without writing client-side JavaScript for the replacement itself.&lt;/p&gt;

&lt;p&gt;Let's dive in!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqjkwpwvwoitaac69eml0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqjkwpwvwoitaac69eml0.png" alt="nyaomaru soccer" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🧩 What are Declarative Partial Updates?
&lt;/h2&gt;

&lt;p&gt;Declarative Partial Updates are, roughly speaking&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A mechanism where you declare part of your HTML as “this area will be replaced later”, and then update only that part with HTML that arrives later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, imagine you want to return the overall page HTML immediately, but some data is slower to fetch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The header is instant&lt;/li&gt;
&lt;li&gt;The layout is fast&lt;/li&gt;
&lt;li&gt;The user profile is also pretty fast&lt;/li&gt;
&lt;li&gt;But only the recommendations API is slow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In that case, we often show a loading state on the client, fetch data, and update the DOM with JavaScript. In &lt;code&gt;React&lt;/code&gt;, you might reach for something like &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With DPU, the server can first return HTML like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Recommendations&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"recommendations"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Loading recommendations...&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The area surrounded by &lt;code&gt;&amp;lt;?start name="recommendations"&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;?end&amp;gt;&lt;/code&gt; is a &lt;strong&gt;region that can be replaced later&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Then, when the recommendations HTML is ready on the server, it can stream HTML like this later.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"recommendations"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Advanced CSS Layouts&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Modern HTML APIs&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Web Performance Basics&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;At that point, the browser finds the region named &lt;code&gt;recommendations&lt;/code&gt; and replaces the loading UI with the contents of this &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Wait, does that feel a bit weird?&lt;/p&gt;

&lt;p&gt;Exactly. The wild part is that &lt;strong&gt;up to this point, we did not write any JavaScript for the replacement, and no framework is involved&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In other words, you no longer need to write client-side JavaScript like this just to replace that area:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#recommendations&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You stream HTML, and the browser understands where that HTML should go.&lt;/p&gt;

&lt;p&gt;That is the interesting part of Declarative Partial Updates.&lt;/p&gt;

&lt;p&gt;DPU also has &lt;code&gt;&amp;lt;?marker&amp;gt;&lt;/code&gt;, not only &lt;code&gt;&amp;lt;?start&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;?end&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;?marker&amp;gt;&lt;/code&gt; is basically a marker that says, “insert later HTML here.”&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="na"&gt;marker&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"placeholder"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"placeholder"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Here is streamed content!&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In this case, the browser finds the marker named &lt;code&gt;placeholder&lt;/code&gt; and inserts the contents of the later &lt;code&gt;&amp;lt;template for="placeholder"&amp;gt;&lt;/code&gt; at that position.&lt;/p&gt;

&lt;p&gt;On the other hand, the &lt;code&gt;&amp;lt;?start&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;?end&amp;gt;&lt;/code&gt; pair used mainly in this article is for showing placeholder content, such as a loading UI, and replacing that entire range later.&lt;/p&gt;

&lt;p&gt;So, roughly&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;?marker&amp;gt;&lt;/code&gt; means “insert here”&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;?start&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;?end&amp;gt;&lt;/code&gt; means “replace this range”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That mental model should be enough to start.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;?marker&amp;gt;&lt;/code&gt; can also be used by leaving the same marker inside streamed content, which lets you append list items one by one. That is pretty interesting too.&lt;/p&gt;

&lt;p&gt;In the first half of this article, I focus mainly on replacing loading UI, so I mostly use &lt;code&gt;&amp;lt;?start&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;?end&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the later sample, I also try an example that uses &lt;code&gt;&amp;lt;?marker&amp;gt;&lt;/code&gt; to append asset links sequentially.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  🤔 Why do we need Declarative Partial Updates?
&lt;/h2&gt;

&lt;p&gt;At this point, you might think&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Wait, frameworks like React or Astro can already do this, right?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yes. They can.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;React&lt;/code&gt; / &lt;code&gt;Vue&lt;/code&gt; / &lt;code&gt;Svelte&lt;/code&gt; / &lt;code&gt;Next&lt;/code&gt; / &lt;code&gt;Remix&lt;/code&gt; / &lt;code&gt;Nuxt&lt;/code&gt; / &lt;code&gt;Astro&lt;/code&gt;, you can show loading states, receive API results, and update parts of the screen. Many of us do this every day.&lt;/p&gt;

&lt;p&gt;But wait a second. A lot of that is implemented by &lt;strong&gt;client-side JavaScript updating the DOM&lt;/strong&gt;, right?&lt;/p&gt;

&lt;p&gt;A common flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The server returns HTML&lt;/li&gt;
&lt;li&gt;The browser displays the HTML&lt;/li&gt;
&lt;li&gt;Client-side JavaScript is loaded&lt;/li&gt;
&lt;li&gt;JavaScript calls an API&lt;/li&gt;
&lt;li&gt;The API result is put into state&lt;/li&gt;
&lt;li&gt;The framework updates the DOM&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Of course, this is an extremely common frontend architecture today.&lt;/p&gt;

&lt;p&gt;But if part of the screen is “something the server can already produce as HTML”, do we really need to make client-side JavaScript do that work every time?&lt;/p&gt;

&lt;p&gt;For example, the recommendations HTML can be generated on the server like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Advanced CSS Layouts&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Modern HTML APIs&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Web Performance Basics&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If all we want to do is send that to the browser, it can feel a bit indirect to do this.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/recommendations&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setRecommendations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You fetch JSON, convert it to HTML on the client, update state, and then update the DOM.&lt;/p&gt;

&lt;p&gt;That is convenient, but it is also a bit roundabout. &lt;/p&gt;

&lt;p&gt;What makes DPU interesting is that it points in another direction&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the server can produce HTML, why not stream that HTML directly and let the browser insert it into the right place?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So DPU is &lt;strong&gt;not a replacement for &lt;code&gt;React&lt;/code&gt; / &lt;code&gt;Vue&lt;/code&gt; / &lt;code&gt;Svelte&lt;/code&gt;&lt;/strong&gt;. It is more like a lower-level browser primitive.&lt;/p&gt;

&lt;p&gt;It may allow the browser to understand, as part of HTML itself, some of the partial update work that frameworks currently handle.&lt;/p&gt;

&lt;p&gt;If that happens, the boundary between client-side responsibility and server-side responsibility becomes even more important.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What can be done with HTML?&lt;/li&gt;
&lt;li&gt;What should be done with JavaScript?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By redesigning these boundaries and declaring things in a way that fits the browser, we may be able to unlock browser capabilities that we are not fully using yet.&lt;/p&gt;

&lt;p&gt;But we should not misunderstand &lt;strong&gt;the client still has responsibilities&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This discussion is not about replacing state management, and interactive features still need JavaScript.&lt;/p&gt;

&lt;p&gt;For example, forms, modals, charts, and similar UI should still be managed on the client side, while static text or server-generated HTML based on slower API responses may be a better fit for HTML streaming.&lt;/p&gt;

&lt;p&gt;The responsibility split becomes more complex, but the UX may improve a lot. This is definitely something worth watching.&lt;/p&gt;


&lt;h2&gt;
  
  
  🏠 This may expand what SPAs can do
&lt;/h2&gt;

&lt;p&gt;DPU is &lt;strong&gt;not about rejecting SPAs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Personally, I see it as something that may expand the possibilities of SPAs.&lt;/p&gt;

&lt;p&gt;Until now, SPAs have often followed this mental model.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fetch data as JSON
    ↓
Put it into client-side state
    ↓
Re-render components
    ↓
Update the DOM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But if DPU and the new HTML insertion / streaming APIs become available, another option appears.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The server returns HTML
    ↓
HTML is streamed
    ↓
The browser inserts it into the existing DOM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That means an SPA could potentially choose different update strategies for different parts of the UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetch JSON and render on the client&lt;/li&gt;
&lt;li&gt;Generate HTML on the server and insert it directly&lt;/li&gt;
&lt;li&gt;Return only the shell initially, then stream slower parts later&lt;/li&gt;
&lt;li&gt;Perform page-transition-like updates with HTML partials&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is really interesting.&lt;/p&gt;

&lt;p&gt;SPAs often carry the image of “doing everything with client-side JavaScript”, but they may become more hybrid in the future. The meaning of the word SPA itself may change.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State that belongs on the client stays on the client&lt;/li&gt;
&lt;li&gt;HTML that can be produced on the server is produced on the server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the browser does not merely display that HTML. It also understands &lt;strong&gt;where and how to insert it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If this direction continues, frontend architecture may become even more flexible and deeper than it is today.&lt;/p&gt;


&lt;h2&gt;
  
  
  🧪 APIs for streaming HTML from JavaScript are also coming
&lt;/h2&gt;

&lt;p&gt;The interesting part of DPU is not only out-of-order streaming with &lt;code&gt;&amp;lt;template for="..."&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Chrome team is also proposing a new set of APIs for inserting and streaming HTML from JavaScript.&lt;/p&gt;

&lt;p&gt;Even today, there are several ways to dynamically insert HTML.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertAdjacentHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;beforeend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But they all have slightly different behavior.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does it overwrite existing content?&lt;/li&gt;
&lt;li&gt;Does it append after the existing content?&lt;/li&gt;
&lt;li&gt;Are &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags executed?&lt;/li&gt;
&lt;li&gt;What happens with sanitization?&lt;/li&gt;
&lt;li&gt;How does it interact with Trusted Types?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are a lot of subtle differences.&lt;/p&gt;

&lt;p&gt;Honestly, I do not think many developers can confidently explain all of those differences from memory. I definitely cannot.&lt;/p&gt;

&lt;p&gt;The new APIs try to make HTML insertion easier to understand with more consistent names.&lt;/p&gt;

&lt;p&gt;For example, static HTML insertion looks like this.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;p&amp;gt;This is a new paragraph&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contentElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#content-to-update&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;contentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newHTML&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is not an API that appends to the end, like &lt;code&gt;insertAdjacentHTML("beforeend", html)&lt;/code&gt;. It replaces the existing contents of &lt;code&gt;contentElement&lt;/code&gt; with &lt;code&gt;newHTML&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A useful way to understand the difference is&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;setHTML()&lt;/code&gt; is the safer API that runs through a sanitizer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setHTMLUnsafe()&lt;/code&gt; disables the sanitizer by default and lets you handle HTML more directly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then comes the really interesting part of streaming versions.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contentElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#content-to-update&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/content.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeThrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoderStream&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streamHTMLUnsafe&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is pretty wild.&lt;/p&gt;

&lt;p&gt;It means we may be able to take HTML returned by &lt;code&gt;fetch()&lt;/code&gt; and stream it directly into an element &lt;strong&gt;without waiting for the entire response to finish downloading&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;SPAs have not benefited much from HTML streaming after the initial document load.&lt;/p&gt;

&lt;p&gt;The initial document can be streamed from the server, but later screen updates often become JSON fetch + client render.&lt;/p&gt;

&lt;p&gt;With APIs like this, SPA updates may also be able to benefit from HTML streaming.&lt;/p&gt;

&lt;p&gt;That is why I think this may expand the possibilities of SPAs.&lt;/p&gt;


&lt;h2&gt;
  
  
  ⚠️ “unsafe” does not mean “never use it”
&lt;/h2&gt;

&lt;p&gt;When you see a name like &lt;code&gt;streamHTMLUnsafe()&lt;/code&gt;, you might think&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Unsafe!? That sounds terrifying.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I thought that too at first.&lt;/p&gt;

&lt;p&gt;But this &lt;code&gt;unsafe&lt;/code&gt; does not mean “you must never use it.” It is closer to a warning that &lt;strong&gt;the sanitizer is disabled by default, so you need to think carefully about whether the input is trusted&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example, it would be dangerous to stream user-generated HTML directly into the DOM.&lt;/p&gt;

&lt;p&gt;But if the HTML is generated by your own trusted server, there may be valid use cases.&lt;/p&gt;

&lt;p&gt;Of course, before using this in production, you need to think carefully about sanitizers, Trusted Types, script execution, browser support, and other security concerns.&lt;/p&gt;


&lt;h2&gt;
  
  
  ➕ What happens when DPU and the streaming APIs are combined?
&lt;/h2&gt;

&lt;p&gt;This is the most interesting part.&lt;/p&gt;

&lt;p&gt;DPU’s &lt;code&gt;&amp;lt;template for="..."&amp;gt;&lt;/code&gt; and the new HTML streaming APIs are &lt;strong&gt;separate APIs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But combining them makes the story even deeper.&lt;/p&gt;

&lt;p&gt;For example, imagine placing placeholders in an SPA screen.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"page-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Loading...&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"page-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Loading content...&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then, during a page update, the server streams HTML.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"page-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Dashboard&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"page-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;section&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;This is the dashboard content.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Instead of manually targeting the DOM with JavaScript.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#page-title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;titleHTML&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#page-content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contentHTML&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;the browser may be able to understand where &lt;code&gt;page-title&lt;/code&gt; and &lt;code&gt;page-content&lt;/code&gt; should be applied.&lt;/p&gt;

&lt;p&gt;In other words, even in SPA-style page transitions, something like this may become possible.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fetch a new page's HTML partial
    ↓
Stream the HTML
    ↓
Update only the necessary places via template for
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Part of the work that frameworks currently handle with Virtual DOM, compilers, or fine-grained reactivity may be shifted to browser-native HTML streaming / partial updates.&lt;/p&gt;

&lt;p&gt;That makes DPU feel like more than just a small HTML API. It feels like something that could expand the design space of frontend architecture.&lt;/p&gt;

&lt;p&gt;Of course, this is still experimental. This is not a “let's all use this in production right now” kind of API.&lt;/p&gt;

&lt;p&gt;But the old assumption&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An SPA fetches JSON and renders on the client&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;may start to change.&lt;/p&gt;

&lt;p&gt;HTML may evolve from “a static document loaded once at the beginning” into “a UI unit that can be streamed, replaced, and updated.”&lt;/p&gt;

&lt;p&gt;That is pretty exciting, right?&lt;/p&gt;


&lt;h2&gt;
  
  
  🚀 Trying it with Node.js DPU samples
&lt;/h2&gt;

&lt;p&gt;So far, we have talked about the concept. I also built a small Node.js sample app to try this out.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;
        nyaomaru
      &lt;/a&gt; / &lt;a href="https://github.com/nyaomaru/declarative-partial-updates-sample" rel="noopener noreferrer"&gt;
        declarative-partial-updates-sample
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Sample repository for DPU (Declarative Partial Updates)
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Declarative Partial Updates sample&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A small Node.js sample app for trying Chrome's experimental Declarative Partial
Updates (DPU) feature.&lt;/p&gt;
&lt;p&gt;Images under &lt;code&gt;public/&lt;/code&gt; are served through the Node.js stream handler at &lt;code&gt;/public/...&lt;/code&gt;
then inserted from later DPU or HTML streaming chunks.&lt;/p&gt;
&lt;p&gt;Styles live in &lt;code&gt;public/styles.css&lt;/code&gt;. Shared timing and image constants live in
&lt;code&gt;features/shared.mjs&lt;/code&gt;, while public asset metadata and GitHub links live in
&lt;code&gt;features/public-assets.mjs&lt;/code&gt; so the sample behavior is easier to inspect and
adjust.&lt;/p&gt;
&lt;p&gt;The server is split by feature:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;features/home/&lt;/code&gt;: DPU document streaming sample HTML and stream chunks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;features/hybrid-shell/&lt;/code&gt;: hybrid shell HTML and route partial HTML&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;features/set-apis/&lt;/code&gt;: static and streaming HTML insertion sample HTML&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;features/public-assets.mjs&lt;/code&gt;: public asset metadata and stream handler&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;features/layout/&lt;/code&gt;: shared document layout HTML&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;features/layout.mjs&lt;/code&gt;: layout template renderer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;features/shared.mjs&lt;/code&gt;: common helpers and demo timing constants&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;public/html-stream.js&lt;/code&gt;: client-side static / streaming HTML insertion helper&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm start&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Open:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="http://localhost:3000/" rel="nofollow noopener noreferrer"&gt;http://localhost:3000/&lt;/a&gt;…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nyaomaru/declarative-partial-updates-sample" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is an experimental feature, so you need Chrome Canary (Chrome v148 or later) or a Chrome build that includes this feature, with the experimental flag enabled.&lt;/p&gt;

&lt;p&gt;Open the following URL in Chrome’s address bar:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;chrome://flags/#enable-experimental-web-platform-features&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Enable it, click “Relaunch”, and then run the sample. This article assumes a supported build with this flag enabled.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2r0obwjpvcguchppde0j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2r0obwjpvcguchppde0j.png" alt=" " width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;The demo app has roughly three pages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/              ... Document streaming
/hybrid-shell  ... Hybrid shell + streamed HTML route updates
/set-apis      ... Static / Streaming HTML APIs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first page, &lt;code&gt;/&lt;/code&gt;, demonstrates DPU out-of-order streaming.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiq3rgn0n0yb4sbyhlcgg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiq3rgn0n0yb4sbyhlcgg.gif" alt=" " width="460" height="628"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The server first returns an HTML shell, and slower server-rendered fragments are streamed later as &lt;code&gt;&amp;lt;template for="..."&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, the initial HTML contains a placeholder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"panel"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Recommendations&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"recommendations"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"loading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Waiting for recommendations...&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, the browser shows the loading UI.&lt;/p&gt;

&lt;p&gt;Then, once the recommendations HTML is ready on the server, it streams a chunk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"recommendations"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Advanced CSS Layouts&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Modern HTML APIs&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Web Performance Basics&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser finds the DPU range named &lt;code&gt;recommendations&lt;/code&gt; and replaces the loading UI with the list.&lt;/p&gt;

&lt;p&gt;On this page, I stream multiple named regions at different timings, not only &lt;code&gt;recommendations&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hero image&lt;/li&gt;
&lt;li&gt;metric&lt;/li&gt;
&lt;li&gt;asset strip&lt;/li&gt;
&lt;li&gt;team activity&lt;/li&gt;
&lt;li&gt;streamed code block&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The asset strip also uses &lt;code&gt;&amp;lt;?marker&amp;gt;&lt;/code&gt;. The initial HTML has a marker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"panel asset-panel"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Streamed public assets&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"asset-grid"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="na"&gt;marker&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"asset-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the server streams chunks one by one whenever each asset link becomes ready.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"asset-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- streamed asset link --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="na"&gt;marker&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"asset-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By leaving the same marker again inside the streamed content, the next asset link can continue to be appended in the same place.&lt;/p&gt;

&lt;p&gt;So HTML does not have to wait until everything is complete from top to bottom. &lt;strong&gt;Fragments can be streamed in later as soon as they are ready.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the really interesting part of DPU.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;template for&amp;gt;&lt;/code&gt; has several subtle rules, including constraints related to parent elements.&lt;/p&gt;

&lt;p&gt;This demo is a sample for exploring the direction of the API, but if you want to use it in production someday, you should carefully check the applicable range of &lt;code&gt;&amp;lt;template for&amp;gt;&lt;/code&gt;, DOM movement during streaming, Trusted Types, sanitizers, and related details.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🌓 Trying a hybrid shell style architecture
&lt;/h2&gt;

&lt;p&gt;Next, &lt;code&gt;/hybrid-shell&lt;/code&gt; demonstrates streaming HTML route updates inside a hybrid shell style architecture.&lt;/p&gt;

&lt;p&gt;This is not a “pure SPA where everything is rendered on the client”. It is closer to an architecture where &lt;strong&gt;the whole document is not reloaded, the URL changes through the History API, and only the route content inside the shell is updated&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The goal is&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Keep client-side state alive, while replacing only the route content with server-generated HTML.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, this page has a counter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  Counter: &lt;span class="nt"&gt;&amp;lt;strong&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"counter"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"increment"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"primary"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Increment&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This counter is managed by client-side JavaScript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#increment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So this is clearly a client-side responsibility.&lt;/p&gt;

&lt;p&gt;On the other hand, route content is fetched from the server as HTML.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;push&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setActiveRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/hybrid-shell/dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/hybrid-shell/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/partials/hybrid-shell/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceChildren&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;streamHTMLUnsafe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeThrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoderStream&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streamHTMLUnsafe&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;template&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceChildren&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cloneNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does is not “fetch JSON and render components on the client.” Instead, &lt;strong&gt;it streams HTML generated by the server directly into the view&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The counter state remains on the client.&lt;/p&gt;

&lt;p&gt;That means we can split responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Interactive state such as the counter belongs to the client&lt;/li&gt;
&lt;li&gt;Server-generated route body can be streamed as HTML&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This feels close to the thinking behind island architecture in frameworks like &lt;code&gt;Astro&lt;/code&gt; / &lt;code&gt;Fresh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of making the client do everything, we separate &lt;strong&gt;HTML that can be returned statically or from the server&lt;/strong&gt; from &lt;strong&gt;interactive client state&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Personally, I find this very interesting.&lt;/p&gt;

&lt;p&gt;At the very least, a shell with SPA-like navigation does not have to mean “everything is rendered by client-side JavaScript.” It may also expand into a hybrid shape that combines client state with streamed HTML.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This &lt;code&gt;/hybrid-shell&lt;/code&gt; sample is &lt;strong&gt;not&lt;/strong&gt; a demo that replaces a range inside the shell using DPU’s &lt;code&gt;&amp;lt;?start&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;template for&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Chrome team article also introduces the possibility of SPA-style page loads by combining &lt;code&gt;streamHTMLUnsafe()&lt;/code&gt; and &lt;code&gt;&amp;lt;template for&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, in my local tests, I could not confirm that streaming &lt;code&gt;&amp;lt;template for="..."&amp;gt;&lt;/code&gt; into &lt;code&gt;streamHTMLUnsafe()&lt;/code&gt; in this shell architecture updates an existing DPU range.&lt;/p&gt;

&lt;p&gt;The SPA-style page load described in the official article seems to point toward an outline page with processing instructions, where the new page’s &lt;code&gt;&amp;lt;template for&amp;gt;&lt;/code&gt; elements are streamed toward the end of the HTML and slotted into those instructions.&lt;/p&gt;

&lt;p&gt;So in this &lt;code&gt;/hybrid-shell&lt;/code&gt; sample, I do not handle DPU range replacement. Instead, I use the HTML streaming insertion API to stream server-generated route partials directly into &lt;code&gt;#hybrid-shell-view&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🎁 Trying Static / Streaming HTML APIs
&lt;/h2&gt;

&lt;p&gt;Finally, &lt;code&gt;/set-apis&lt;/code&gt; tries the new HTML insertion / streaming APIs separately from DPU’s out-of-order streaming.&lt;/p&gt;

&lt;p&gt;This page first checks whether the browser has the APIs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;support&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
  &amp;lt;ul class="list"&amp;gt;
    &amp;lt;li&amp;gt;setHTMLUnsafe: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;setHTMLUnsafe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/li&amp;gt;
    &amp;lt;li&amp;gt;streamHTMLUnsafe: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;streamHTMLUnsafe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/li&amp;gt;
  &amp;lt;/ul&amp;gt;
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For static insertion, if &lt;code&gt;setHTMLUnsafe()&lt;/code&gt; is available, the demo uses it. Otherwise, it falls back to &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; and &lt;code&gt;replaceChildren()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;setHTMLUnsafe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;staticTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHTMLUnsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;template&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;markup&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;staticTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceChildren&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cloneNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For streaming insertion, it pipes the fetched HTML response directly into &lt;code&gt;streamHTMLUnsafe()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/partials/set-apis/stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;streamTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceChildren&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;streamHTMLUnsafe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeThrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoderStream&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;streamTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streamHTMLUnsafe&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this works, we can stream chunks into an existing element as they arrive, &lt;strong&gt;instead of waiting until the entire HTML response has downloaded and inserting it all at once&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Traditionally, SPAs could benefit from HTML streaming for the initial document, but subsequent screen updates often became JSON fetch + client render.&lt;/p&gt;

&lt;p&gt;With APIs like this, SPAs may be able to benefit from HTML streaming even for later updates.&lt;/p&gt;

&lt;p&gt;That is the exciting part of DPU and the new HTML streaming APIs.&lt;/p&gt;

&lt;p&gt;The static insertion partial is returned from &lt;code&gt;/partials/set-apis/static&lt;/code&gt;, and the streaming insertion partial is returned from &lt;code&gt;/partials/set-apis/stream&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Summary
&lt;/h2&gt;

&lt;p&gt;In this article, I looked at Declarative Partial Updates introduced by the Chrome team.&lt;/p&gt;

&lt;p&gt;Declarative Partial Updates are, roughly&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A mechanism where you declare part of your HTML as “this area will be replaced later”, and then update only that part with HTML that arrives later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Of course, DPU does not replace &lt;code&gt;React&lt;/code&gt; / &lt;code&gt;Vue&lt;/code&gt; / &lt;code&gt;Svelte&lt;/code&gt; / &lt;code&gt;Next&lt;/code&gt; / &lt;code&gt;Remix&lt;/code&gt; / &lt;code&gt;Nuxt&lt;/code&gt; / &lt;code&gt;Astro&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Interactive UI, client-side state management, forms, modals, charts, drag and drop, and similar features still need JavaScript.&lt;/p&gt;

&lt;p&gt;But on the other hand, there may be more cases where we do not need to handle everything with client-side JavaScript.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML that can be generated on the server should be generated on the server&lt;/li&gt;
&lt;li&gt;State that should be managed on the client should be managed on the client&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the browser may come to understand &lt;strong&gt;not only how to display HTML, but also where streamed HTML should be inserted&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If this responsibility split progresses, frontend architecture may become more hybrid.&lt;/p&gt;

&lt;p&gt;Especially for SPAs, there may be another path beyond&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fetch JSON and render on the client&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead, we may also be able to&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fetch / stream HTML partials and update via browser-native behavior&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I do not see this as the end of SPAs. I see it as something that may expand what SPAs can do.&lt;/p&gt;

&lt;p&gt;This is still experimental, so it is not an API to use in production right away.&lt;/p&gt;

&lt;p&gt;But the direction where HTML changes from “a document loaded once at the beginning” into “a UI unit that can be streamed, replaced, and updated” feels very important for the future of frontend development.&lt;/p&gt;

&lt;p&gt;DPU is definitely something to keep an eye on. 😺&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://developer.chrome.com/blog/declarative-partial-updates?hl=en" rel="noopener noreferrer"&gt;https://developer.chrome.com/blog/declarative-partial-updates?hl=en&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/WICG/declarative-partial-updates" rel="noopener noreferrer"&gt;https://github.com/WICG/declarative-partial-updates&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/GoogleChromeLabs/template-for-polyfill" rel="noopener noreferrer"&gt;https://github.com/GoogleChromeLabs/template-for-polyfill&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/GoogleChromeLabs/html-setters-polyfill" rel="noopener noreferrer"&gt;https://github.com/GoogleChromeLabs/html-setters-polyfill&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/declarative-partial-updates-sample" rel="noopener noreferrer"&gt;https://github.com/nyaomaru/declarative-partial-updates-sample&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>html</category>
      <category>browser</category>
      <category>node</category>
    </item>
    <item>
      <title>The Difference Between satisfies, as, and Generics in TypeScript</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Wed, 17 Jun 2026 12:43:50 +0000</pubDate>
      <link>https://dev.to/nyaomaru/the-difference-between-satisfies-as-and-generics-in-typescript-5f9f</link>
      <guid>https://dev.to/nyaomaru/the-difference-between-satisfies-as-and-generics-in-typescript-5f9f</guid>
      <description>&lt;p&gt;Hoi hoi!&lt;/p&gt;

&lt;p&gt;I'm &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;@nyaomaru&lt;/a&gt;, a frontend engineer who recently obsessed with playing video game of "inscryption". 🃏&lt;/p&gt;

&lt;p&gt;In TypeScript, there are several syntaxes that look like they are "specifying a type."&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;as&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;satisfies&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;generics using &lt;code&gt;T extends xxx&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But these are not the same thing.&lt;/p&gt;

&lt;p&gt;Roughly speaking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;as&lt;/code&gt; asks TypeScript to &lt;strong&gt;trust you&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;satisfies&lt;/code&gt; asks TypeScript to &lt;strong&gt;check the value&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;generics let you &lt;strong&gt;encapsulate that check inside an API&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They all look type-related, but the responsibility lives in different places.&lt;/p&gt;

&lt;p&gt;You may see AI-generated code overusing &lt;code&gt;as&lt;/code&gt;, or codebases using &lt;code&gt;satisfies&lt;/code&gt; casually, or libraries using generics to enforce stricter APIs. These patterns appear quite often in real-world development.&lt;/p&gt;

&lt;p&gt;In this article, I want to explain the difference between &lt;code&gt;as&lt;/code&gt;, &lt;code&gt;satisfies&lt;/code&gt;, and generics not as "ways to specify a type," but as &lt;strong&gt;ways to decide where type responsibility should live&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let's jump in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe3kkusmu97rasjez8s9s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe3kkusmu97rasjez8s9s.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🙏 &lt;code&gt;as&lt;/code&gt; asks TypeScript to trust you
&lt;/h2&gt;

&lt;p&gt;In TypeScript, &lt;code&gt;as&lt;/code&gt; lets you tell the compiler:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please treat this value as this type, please 😭&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, the runtime value below is still a &lt;code&gt;string&lt;/code&gt;, but TypeScript treats it as a &lt;code&gt;number&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "string"&lt;/span&gt;

&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// TypeScript thinks this is a number, so there is no compile-time error.&lt;/span&gt;
&lt;span class="c1"&gt;// But at runtime, this throws: "value.toFixed is not a function"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The important point is 👇&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;as&lt;/code&gt; does not transform the runtime value.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So using it for external data can be very dangerous.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This does not check whether the parsed value is actually a &lt;code&gt;User&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It only tells TypeScript:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Trust me. This is a &lt;code&gt;User&lt;/code&gt;. Maybe.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words, &lt;code&gt;as&lt;/code&gt; is for cases where &lt;strong&gt;a human knows something TypeScript cannot know&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is useful, but it does not guarantee anything by itself.&lt;/p&gt;

&lt;p&gt;If you write &lt;code&gt;as&lt;/code&gt;, &lt;strong&gt;the responsibility is on you.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🔍 &lt;code&gt;satisfies&lt;/code&gt; asks TypeScript to check the value
&lt;/h2&gt;

&lt;p&gt;Now let's look at &lt;code&gt;satisfies&lt;/code&gt;, which was introduced in TypeScript 4.9.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RouteConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
  &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Home&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;about&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/about&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;About&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;RouteConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With this, TypeScript checks whether the declared value matches the expected type structure.&lt;/p&gt;

&lt;p&gt;For example, if you add a property that does not exist on a route item, TypeScript reports an error.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Home&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;about&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/about&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;About&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;nyaomaru&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Type error: nyaomaru does not exist on the route item type 😼&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;RouteConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The interesting part is 👇&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Even though &lt;code&gt;routes&lt;/code&gt; is checked against &lt;code&gt;RouteConfig&lt;/code&gt;, the type of &lt;code&gt;routes&lt;/code&gt; itself is not collapsed into &lt;code&gt;RouteConfig&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;routes&lt;/code&gt; is checked as &lt;code&gt;RouteConfig&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But the inferred type of &lt;code&gt;routes&lt;/code&gt; remains close to the actual object literal you wrote.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;keyof typeof routes&lt;/code&gt; becomes the union of the actual keys, not just &lt;code&gt;string&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RouteName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// "home" | "about"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In other words, &lt;code&gt;satisfies&lt;/code&gt; checks that a value satisfies a given type while preserving the original inference as much as possible.&lt;/p&gt;

&lt;p&gt;Compare that with a regular type annotation.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RouteConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Home&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;about&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/about&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;About&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RouteName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;When you annotate &lt;code&gt;routes&lt;/code&gt; as &lt;code&gt;RouteConfig&lt;/code&gt;, TypeScript treats it as &lt;code&gt;RouteConfig&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;RouteConfig&lt;/code&gt; is a &lt;code&gt;Record&amp;lt;string, ...&amp;gt;&lt;/code&gt;, &lt;code&gt;RouteName&lt;/code&gt; becomes &lt;code&gt;string&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So when you want to validate a value against a type while keeping the concrete information from the actual value, &lt;code&gt;satisfies&lt;/code&gt; is often a better fit.&lt;/p&gt;

&lt;p&gt;In short:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;satisfies&lt;/code&gt; checks that a value matches a type without unnecessarily widening the value's inferred type.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🎁 Generics encapsulate the check inside an API
&lt;/h2&gt;

&lt;p&gt;At this point, you might think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Okay, then I should just use &lt;code&gt;satisfies&lt;/code&gt; everywhere. 😹&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And yes, sometimes that works well.&lt;/p&gt;

&lt;p&gt;But writing &lt;code&gt;satisfies&lt;/code&gt; every time can be annoying.&lt;br&gt;
And if someone forgets to write it, the intended check disappears.&lt;/p&gt;

&lt;p&gt;So what if we move that check into a function?&lt;/p&gt;

&lt;p&gt;That is where generics come in.&lt;/p&gt;

&lt;p&gt;As a quick recap, we could write this&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Home&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;RouteConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But we can also wrap the pattern inside a function using generics.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;defineRoutes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;RouteConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineRoutes&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Home&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Nice and clean.&lt;/p&gt;

&lt;p&gt;Now we get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validation that the value matches &lt;code&gt;RouteConfig&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;preservation of the concrete key &lt;code&gt;"home"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;no need for users to write &lt;code&gt;satisfies&lt;/code&gt; every time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But then this appears&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;RouteConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;What is going on here???&lt;/p&gt;

&lt;p&gt;Let's break it down.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;T extends RouteConfig&lt;/code&gt; means: "&lt;code&gt;T&lt;/code&gt; must be assignable to &lt;code&gt;RouteConfig&lt;/code&gt;."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;const T&lt;/code&gt; asks TypeScript to infer object literals and array literals as narrowly as possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here, &lt;code&gt;const&lt;/code&gt; is not the same as a &lt;code&gt;const&lt;/code&gt; variable declaration.&lt;/p&gt;

&lt;p&gt;It is not about whether a value can be reassigned.&lt;/p&gt;

&lt;p&gt;It is about making generic type parameter inference preserve literal information as much as possible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8mu565dfikencnwt9ym9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8mu565dfikencnwt9ym9.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  📊 Comparing the three with config objects
&lt;/h2&gt;

&lt;p&gt;Here is the comparison so far.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Syntax&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Main responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;as Type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Asks TypeScript to believe the value is that type&lt;/td&gt;
&lt;td&gt;The person writing the code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;satisfies Type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Checks whether the value matches the type&lt;/td&gt;
&lt;td&gt;The value declaration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;T extends Type&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Constrains what an API can accept&lt;/td&gt;
&lt;td&gt;The API designer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So, in casual terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;as&lt;/code&gt; means: "Trust me. 🙏"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;satisfies&lt;/code&gt; means: "Please check if this is correct. 🔍"&lt;/li&gt;
&lt;li&gt;generics mean: "Only pass values that fit this API. 🎁"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So far, we have looked at config objects.&lt;/p&gt;

&lt;p&gt;But in real-world development, this difference becomes even more important when dealing with &lt;strong&gt;external data&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;API responses, &lt;code&gt;localStorage&lt;/code&gt;, URL params, and form input cannot be protected by TypeScript types alone.&lt;/p&gt;

&lt;p&gt;For those values, we need runtime guards.&lt;/p&gt;

&lt;p&gt;And once we start writing runtime guards by hand, another problem appears 👇&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The existing TypeScript type and the handwritten schema can drift apart.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🤔 Thinking through &lt;code&gt;typedStruct&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This becomes easier to understand if we use a small validator API as an example.&lt;/p&gt;

&lt;p&gt;I'll use my type guard library, &lt;code&gt;is-kit&lt;/code&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;
        nyaomaru
      &lt;/a&gt; / &lt;a href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;
        is-kit
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Lightweight, zero-dependency toolkit for building `isFoo` style type guards in TypeScript. Runtime-safe 🛡️, composable 🧩, and ergonomic ✨. npm -&amp;gt; https://www.npmjs.com/package/is-kit
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;is-kit&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/nyaomaru/is-kit/main/docs/public/iskit_image.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fis-kit%2Fmain%2Fdocs%2Fpublic%2Fiskit_image.png" width="600" alt="is-kit logo"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://www.npmjs.com/package/is-kit" rel="nofollow noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/cb829c46f855a6f184f255391ea5fe58bb6ed17112e34d4856acd61dc63179c2/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f69732d6b69742e737667" alt="npm version"&gt;
  &lt;/a&gt;
  &lt;a href="https://jsr.io/@nyaomaru/is-kit" rel="nofollow noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/b6648f6bf32eb9dff2136ce8538ccb4cc6af537c1127292b5fb0de416f7a69b4/68747470733a2f2f696d672e736869656c64732e696f2f6a73722f762f406e79616f6d6172752f69732d6b6974" alt="JSR"&gt;
  &lt;/a&gt;
  &lt;a href="https://www.npmjs.com/package/is-kit" rel="nofollow noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/9b30eeaac46ebced99b5e0dc290fd045bc34c738c66b5d95c4b6ee8fed358b34/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f64742f69732d6b69742e737667" alt="npm downloads"&gt;
  &lt;/a&gt;
  &lt;a href="https://github.com/nyaomaru/is-kit/blob/main/LICENSE" rel="noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/631cdf8c56ad84dd8657fe3371b93015b5474c517422f968ff4ec944bb28bf2f/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f6c2f69732d6b69742e7376673f73616e6974697a653d74727565" alt="License"&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; is a lightweight, zero-dependency toolkit for building reusable TypeScript &lt;strong&gt;type guards&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It helps you write small &lt;code&gt;isFoo&lt;/code&gt; functions, compose them into &lt;strong&gt;richer runtime checks&lt;/strong&gt;, and keep &lt;strong&gt;TypeScript narrowing&lt;/strong&gt; natural inside regular control flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime-safe&lt;/strong&gt; 🛡️, &lt;strong&gt;composable&lt;/strong&gt; 🧩, and &lt;strong&gt;ergonomic&lt;/strong&gt; ✨ without asking you to adopt a heavy schema workflow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build and reuse &lt;strong&gt;typed guards&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose guards&lt;/strong&gt; with &lt;code&gt;and&lt;/code&gt;, &lt;code&gt;or&lt;/code&gt;, &lt;code&gt;not&lt;/code&gt;, &lt;code&gt;oneOf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate object&lt;/strong&gt; shapes and collections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parse or assert&lt;/strong&gt; &lt;code&gt;unknown&lt;/code&gt; values without a large schema framework&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://is-kit-docs.vercel.app/" rel="nofollow noopener noreferrer"&gt;📚 Documentation Site&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Best for &lt;strong&gt;app-internal narrowing, filtering, and reusable guards&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🤔 Why use &lt;code&gt;is-kit&lt;/code&gt;?&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Tired of rewriting the same &lt;code&gt;isFoo&lt;/code&gt; checks again and again?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; is a good fit when you want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;write reusable &lt;code&gt;isX&lt;/code&gt;&lt;/strong&gt; functions instead of one-off inline checks&lt;/li&gt;
&lt;li&gt;keep runtime validation &lt;strong&gt;lightweight and dependency-free&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;narrow values directly&lt;/strong&gt; in &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, and other TypeScript control flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;compose validation logic&lt;/strong&gt;…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; is a lightweight library for building small type guards like &lt;code&gt;isString&lt;/code&gt; and &lt;code&gt;isNumber&lt;/code&gt;, then composing them with helpers like &lt;code&gt;and&lt;/code&gt;, &lt;code&gt;or&lt;/code&gt;, and &lt;code&gt;struct&lt;/code&gt;, while still allowing TypeScript narrowing to work naturally.&lt;/p&gt;

&lt;p&gt;Suppose an external API returns a &lt;code&gt;User&lt;/code&gt; like this&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With a normal &lt;code&gt;struct&lt;/code&gt;, we can create a guard for that object shape.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is useful.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;struct&lt;/code&gt; can infer the return type from the schema, so &lt;code&gt;isUser&lt;/code&gt; can validate a value that looks like a &lt;code&gt;User&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But there is one problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is easy for the schema and the existing type to drift apart.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example, suppose the &lt;code&gt;User&lt;/code&gt; type already exists, but we accidentally write the wrong key in the schema.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This &lt;code&gt;struct&lt;/code&gt; call itself is valid.&lt;/p&gt;

&lt;p&gt;From the perspective of &lt;code&gt;struct&lt;/code&gt;, this is a perfectly valid schema for:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;So &lt;code&gt;struct&lt;/code&gt; is useful for creating a type from a schema.&lt;/p&gt;

&lt;p&gt;But it does not synchronize that schema with an existing &lt;code&gt;User&lt;/code&gt; type.&lt;/p&gt;

&lt;p&gt;A common dangerous workaround is to use &lt;code&gt;as&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Predicate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is extremely dangerous. ⚠️⚠️⚠️&lt;/p&gt;

&lt;p&gt;The schema says &lt;code&gt;displayName&lt;/code&gt;, but we are telling TypeScript&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Trust me. This is a guard for &lt;code&gt;User&lt;/code&gt;. Please 🙏&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That &lt;code&gt;as&lt;/code&gt; is not a check.&lt;/p&gt;

&lt;p&gt;It is just a self-declaration.&lt;/p&gt;

&lt;p&gt;Now, &lt;code&gt;satisfies&lt;/code&gt; can improve this.&lt;/p&gt;

&lt;p&gt;If the library exposes a schema shape type, we could write something like:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;TypedStructShape&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now TypeScript checks whether the schema corresponds to &lt;code&gt;User&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If we write an unknown key or forget a required key, TypeScript can report an error.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// name is missing, and displayName does not exist on User&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;TypedStructShape&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is better.&lt;/p&gt;

&lt;p&gt;But writing this every time is a bit annoying.&lt;/p&gt;

&lt;p&gt;Also, if the user forgets to write &lt;code&gt;satisfies TypedStructShape&amp;lt;User&amp;gt;&lt;/code&gt;, the schema and the type can drift apart again.&lt;/p&gt;

&lt;p&gt;So we can use generics to encapsulate this check inside the API.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;typedStruct&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With this shape, users do not need to write &lt;code&gt;satisfies TypedStructShape&amp;lt;User&amp;gt;&lt;/code&gt; every time.&lt;/p&gt;

&lt;p&gt;Instead, the &lt;code&gt;typedStruct&amp;lt;User&amp;gt;()&lt;/code&gt; API itself checks 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this schema correspond to &lt;code&gt;User&lt;/code&gt;?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, using the framing from earlier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;we are not using &lt;code&gt;as&lt;/code&gt; to ask TypeScript to trust us&lt;/li&gt;
&lt;li&gt;we are not asking users to write &lt;code&gt;satisfies&lt;/code&gt; every time&lt;/li&gt;
&lt;li&gt;we are using generics to encapsulate the check inside an API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the core idea.&lt;/p&gt;




&lt;p&gt;Let's look a little more concretely.&lt;/p&gt;

&lt;p&gt;The implementation idea for &lt;code&gt;typedStruct&lt;/code&gt; looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OptionalKeys&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// WHY: Optional keys accept an empty object when picked in isolation.&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nb"&gt;Pick&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}[&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RequiredKeys&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Exclude&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OptionalKeys&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TypedStructShape&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;Extract&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RequiredKeys&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Predicate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;Extract&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;OptionalKeys&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;OptionalSchemaField&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="nx"&gt;Predicate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;NoExtraKeys&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Shape&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;Exclude&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;Shape&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TypedStructFields&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
  &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;S&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;TypedStructShape&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NoExtraKeys&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TypedStructShape&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;typedStruct&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;TypedStructShape&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TypedStructFields&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;exact&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Predicate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InferSchema&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TypedStructFields&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;// WHY: `typedStruct` keeps `struct` runtime behavior while using the target&lt;/span&gt;
    &lt;span class="c1"&gt;// type only to check that hand-written guard fields stay in sync.&lt;/span&gt;
    &lt;span class="nf"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We can ignore the small details for now.&lt;/p&gt;

&lt;p&gt;The important part is this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;typedStruct&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;First, &lt;code&gt;typedStruct&lt;/code&gt; receives the target type &lt;code&gt;T&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then this part:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;TypedStructShape&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;means the actual schema &lt;code&gt;S&lt;/code&gt; passed by the user must correspond to &lt;code&gt;T&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But here is an important detail&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;S extends TypedStructShape&amp;lt;T&amp;gt;&lt;/code&gt; alone cannot completely prevent extra keys.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;TypeScript uses structural typing.&lt;/p&gt;

&lt;p&gt;If a type has the required properties, it may still satisfy a constraint even if it also has additional properties.&lt;/p&gt;

&lt;p&gt;For example, if &lt;code&gt;User&lt;/code&gt; is &lt;code&gt;{ id: string; name: string }&lt;/code&gt;, then a schema like &lt;code&gt;{ id, name, displayName }&lt;/code&gt; has all required keys and one extra key.&lt;/p&gt;

&lt;p&gt;The constraint &lt;code&gt;S extends TypedStructShape&amp;lt;User&amp;gt;&lt;/code&gt; alone may still allow that.&lt;/p&gt;

&lt;p&gt;So we use &lt;code&gt;NoExtraKeys&lt;/code&gt; to make keys that do not exist in &lt;code&gt;TypedStructShape&amp;lt;T&amp;gt;&lt;/code&gt; become &lt;code&gt;never&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now let's look at how &lt;code&gt;TypedStructShape&amp;lt;T&amp;gt;&lt;/code&gt; works.&lt;/p&gt;

&lt;p&gt;It separates required keys and optional keys from &lt;code&gt;T&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OptionalKeys&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nb"&gt;Pick&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}[&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RequiredKeys&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Exclude&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OptionalKeys&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;OptionalKeys&amp;lt;T&amp;gt;&lt;/code&gt; is used to pick optional properties like &lt;code&gt;name?: string&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;TypedStructShape&amp;lt;T&amp;gt;&lt;/code&gt; treats the keys differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;required keys become &lt;code&gt;Predicate&amp;lt;T[K]&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;optional keys become &lt;code&gt;OptionalSchemaField&amp;lt;Predicate&amp;lt;T[K]&amp;gt;&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means if a key is optional in the target type, the schema must also mark it as optional using something like &lt;code&gt;optionalKey(...)&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TypedStructShape&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;Extract&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RequiredKeys&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Predicate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;Extract&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;OptionalKeys&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;OptionalSchemaField&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="nx"&gt;Predicate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here, &lt;code&gt;OptionalSchemaField&lt;/code&gt; is the internal type used to represent a schema field created by a helper like &lt;code&gt;optionalKey(...)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, &lt;code&gt;TypedStructFields&amp;lt;T, S&amp;gt;&lt;/code&gt; prevents schema fields from having keys that do not exist in &lt;code&gt;T&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Roughly speaking, &lt;code&gt;NoExtraKeys&lt;/code&gt; takes keys that exist in &lt;code&gt;S&lt;/code&gt; but not in &lt;code&gt;Shape&lt;/code&gt;, and turns them into &lt;code&gt;never&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, if &lt;code&gt;User&lt;/code&gt; does not have &lt;code&gt;displayName&lt;/code&gt;, but the schema includes &lt;code&gt;displayName&lt;/code&gt;, that field becomes &lt;code&gt;never&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since a normal guard cannot be assigned to &lt;code&gt;never&lt;/code&gt;, TypeScript rejects the extra key.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;NoExtraKeys&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Shape&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;Exclude&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;Shape&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TypedStructFields&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
  &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;S&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;TypedStructShape&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NoExtraKeys&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TypedStructShape&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;So &lt;code&gt;typedStruct&lt;/code&gt; is not just a helper that adds a type to &lt;code&gt;struct&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is an API for making an existing object type and a handwritten runtime guard harder to drift apart.&lt;/p&gt;

&lt;p&gt;One important point 👇&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;typedStruct&lt;/code&gt; does not automatically generate a runtime validator.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;TypeScript types do not exist at runtime.&lt;/p&gt;

&lt;p&gt;Also, we cannot automatically generate &lt;code&gt;isString&lt;/code&gt; or &lt;code&gt;isNumber&lt;/code&gt; from the &lt;code&gt;User&lt;/code&gt; type.&lt;/p&gt;

&lt;p&gt;So the guard itself still has to be written by hand.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;typedStruct&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But the API can check whether the handwritten schema is still in sync with &lt;code&gt;User&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's the tasty part of &lt;code&gt;typedStruct&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is especially useful when you already have API response types generated from something like OpenAPI.&lt;/p&gt;

&lt;p&gt;Here, imagine &lt;code&gt;ApiResponse&lt;/code&gt; is a generated response type.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ArticleResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ApiResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/articles/{id}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isArticleResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;typedStruct&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ArticleResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;optionalKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here, &lt;code&gt;optionalKey(isString)&lt;/code&gt; means the object key itself may be missing.&lt;/p&gt;

&lt;p&gt;But if the key exists, the value must pass &lt;code&gt;isString&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So as a runtime check, this does not allow &lt;code&gt;{ summary: undefined }&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if &lt;code&gt;summary&lt;/code&gt; exists, it must be a &lt;code&gt;string&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;if &lt;code&gt;summary&lt;/code&gt; does not exist, that is OK&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a small but important distinction.&lt;/p&gt;

&lt;p&gt;The idea is not ❌&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The TypeScript property is optional, so I can omit the schema field.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead, the idea is ✅&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The runtime guard should explicitly say that this key is optional.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That way, when the target type changes, the mismatch becomes easier to notice.&lt;/p&gt;

&lt;p&gt;Also, the compile-time key check in &lt;code&gt;typedStruct&lt;/code&gt; and the runtime check in &lt;code&gt;struct(..., { exact: true })&lt;/code&gt; are different things.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;typedStruct&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;exact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;NoExtraKeys&lt;/code&gt; is a compile-time check that prevents the schema author from writing extra keys in the schema.&lt;/p&gt;

&lt;p&gt;On the other hand, &lt;code&gt;{ exact: true }&lt;/code&gt; is a runtime check that rejects actual runtime objects if they contain keys outside the schema.&lt;/p&gt;

&lt;p&gt;They may look similar, but they check different things.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NoExtraKeys&lt;/code&gt; checks mistakes in the handwritten schema&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{ exact: true }&lt;/code&gt; checks extra keys in runtime data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fubximlmkrvqirlfv4owh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fubximlmkrvqirlfv4owh.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is exactly the theme of the article.&lt;/p&gt;

&lt;p&gt;Instead of silencing TypeScript with &lt;code&gt;as&lt;/code&gt;, we take a &lt;code&gt;satisfies&lt;/code&gt;-like check and encapsulate it inside an API with generics.&lt;/p&gt;

&lt;p&gt;One more important design point 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The return type is not directly forced to &lt;code&gt;T&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead, it is inferred from the actual &lt;code&gt;fields&lt;/code&gt; passed to the function.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Predicate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InferSchema&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TypedStructFields&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;So &lt;code&gt;T&lt;/code&gt; is used to check whether the schema is in sync with the existing type.&lt;/p&gt;

&lt;p&gt;But the actual guard type is inferred from the schema, just like &lt;code&gt;struct&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In other words, we are not doing this&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Predicate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Instead, the existing type synchronization is handled by the generic constraint, while the actual guard type is derived from the schema.&lt;/p&gt;


&lt;h2&gt;
  
  
  🎯 Summary
&lt;/h2&gt;

&lt;p&gt;In this article, we looked at the difference between &lt;code&gt;as&lt;/code&gt;, &lt;code&gt;satisfies&lt;/code&gt;, and generics through the lens of &lt;strong&gt;where type responsibility lives&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Syntax&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Where the responsibility lives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;as Type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Asks TypeScript to trust you 🙏&lt;/td&gt;
&lt;td&gt;The person writing the code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;satisfies Type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Checks whether a value matches a type 🔍&lt;/td&gt;
&lt;td&gt;The value declaration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;T extends Type&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Constrains what an API accepts 🎁&lt;/td&gt;
&lt;td&gt;The API designer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;as&lt;/code&gt; is not evil.&lt;/p&gt;

&lt;p&gt;Sometimes TypeScript cannot know something, and a human has to take responsibility for that missing information.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;as&lt;/code&gt; is not a check.&lt;/p&gt;

&lt;p&gt;When you use &lt;code&gt;as&lt;/code&gt; for external data or handwritten schemas, the code can look type-safe while skipping the actual validation.&lt;/p&gt;

&lt;p&gt;AI-generated code often papers over uncertainty with &lt;code&gt;as&lt;/code&gt;, so this is something worth watching for.&lt;/p&gt;

&lt;p&gt;When you really want to check whether a value has the right shape, first consider whether &lt;code&gt;satisfies&lt;/code&gt; can help.&lt;/p&gt;

&lt;p&gt;And when you find yourself repeating the same &lt;code&gt;satisfies&lt;/code&gt; pattern again and again, consider moving that constraint into an API with generics.&lt;/p&gt;

&lt;p&gt;Personally, I find this framing useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;as&lt;/code&gt; means: "Trust me."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;satisfies&lt;/code&gt; means: "Please check if this is correct."&lt;/li&gt;
&lt;li&gt;generics mean: "Only pass values that fit this API."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;typedStruct&lt;/code&gt; in &lt;code&gt;is-kit&lt;/code&gt; is a good example of this difference.&lt;/p&gt;

&lt;p&gt;When you want to keep an existing object type and a runtime guard in sync, do not silence TypeScript with &lt;code&gt;as&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead, you can take a &lt;code&gt;satisfies&lt;/code&gt;-like check and encapsulate it inside an API using generics.&lt;/p&gt;

&lt;p&gt;This makes the safer way the natural way to use the API, and moves type responsibility closer to the API design.&lt;/p&gt;

&lt;p&gt;Happy TypeScript life. 🫰&lt;/p&gt;


&lt;h2&gt;
  
  
  Bonus
&lt;/h2&gt;

&lt;p&gt;The user-defined type guard library used in this article is &lt;code&gt;is-kit&lt;/code&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;
        nyaomaru
      &lt;/a&gt; / &lt;a href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;
        is-kit
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Lightweight, zero-dependency toolkit for building `isFoo` style type guards in TypeScript. Runtime-safe 🛡️, composable 🧩, and ergonomic ✨. npm -&amp;gt; https://www.npmjs.com/package/is-kit
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;is-kit&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/nyaomaru/is-kit/main/docs/public/iskit_image.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fis-kit%2Fmain%2Fdocs%2Fpublic%2Fiskit_image.png" width="600" alt="is-kit logo"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://www.npmjs.com/package/is-kit" rel="nofollow noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/cb829c46f855a6f184f255391ea5fe58bb6ed17112e34d4856acd61dc63179c2/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f69732d6b69742e737667" alt="npm version"&gt;
  &lt;/a&gt;
  &lt;a href="https://jsr.io/@nyaomaru/is-kit" rel="nofollow noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/b6648f6bf32eb9dff2136ce8538ccb4cc6af537c1127292b5fb0de416f7a69b4/68747470733a2f2f696d672e736869656c64732e696f2f6a73722f762f406e79616f6d6172752f69732d6b6974" alt="JSR"&gt;
  &lt;/a&gt;
  &lt;a href="https://www.npmjs.com/package/is-kit" rel="nofollow noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/9b30eeaac46ebced99b5e0dc290fd045bc34c738c66b5d95c4b6ee8fed358b34/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f64742f69732d6b69742e737667" alt="npm downloads"&gt;
  &lt;/a&gt;
  &lt;a href="https://github.com/nyaomaru/is-kit/blob/main/LICENSE" rel="noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/631cdf8c56ad84dd8657fe3371b93015b5474c517422f968ff4ec944bb28bf2f/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f6c2f69732d6b69742e7376673f73616e6974697a653d74727565" alt="License"&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; is a lightweight, zero-dependency toolkit for building reusable TypeScript &lt;strong&gt;type guards&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It helps you write small &lt;code&gt;isFoo&lt;/code&gt; functions, compose them into &lt;strong&gt;richer runtime checks&lt;/strong&gt;, and keep &lt;strong&gt;TypeScript narrowing&lt;/strong&gt; natural inside regular control flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime-safe&lt;/strong&gt; 🛡️, &lt;strong&gt;composable&lt;/strong&gt; 🧩, and &lt;strong&gt;ergonomic&lt;/strong&gt; ✨ without asking you to adopt a heavy schema workflow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build and reuse &lt;strong&gt;typed guards&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose guards&lt;/strong&gt; with &lt;code&gt;and&lt;/code&gt;, &lt;code&gt;or&lt;/code&gt;, &lt;code&gt;not&lt;/code&gt;, &lt;code&gt;oneOf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate object&lt;/strong&gt; shapes and collections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parse or assert&lt;/strong&gt; &lt;code&gt;unknown&lt;/code&gt; values without a large schema framework&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://is-kit-docs.vercel.app/" rel="nofollow noopener noreferrer"&gt;📚 Documentation Site&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Best for &lt;strong&gt;app-internal narrowing, filtering, and reusable guards&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🤔 Why use &lt;code&gt;is-kit&lt;/code&gt;?&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Tired of rewriting the same &lt;code&gt;isFoo&lt;/code&gt; checks again and again?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; is a good fit when you want to:&lt;/p&gt;


&lt;ul&gt;

&lt;li&gt;

&lt;strong&gt;write reusable &lt;code&gt;isX&lt;/code&gt;&lt;/strong&gt; functions instead of one-off inline checks&lt;/li&gt;

&lt;li&gt;keep runtime validation &lt;strong&gt;lightweight and dependency-free&lt;/strong&gt;
&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;narrow values directly&lt;/strong&gt; in &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, and other TypeScript control flow&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;compose validation logic&lt;/strong&gt;…&lt;/li&gt;

&lt;/ul&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;code&gt;typedStruct&lt;/code&gt; was added in v1.8, so please give it a try. 😻&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>programming</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>CHANGELOG.md is for Both Humans and AI Now, So Let’s Automate It</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Wed, 03 Jun 2026 12:33:59 +0000</pubDate>
      <link>https://dev.to/nyaomaru/changelogmd-is-for-both-humans-and-ai-now-so-lets-automate-it-18dp</link>
      <guid>https://dev.to/nyaomaru/changelogmd-is-for-both-humans-and-ai-now-so-lets-automate-it-18dp</guid>
      <description>&lt;p&gt;Hoi hoi!&lt;/p&gt;

&lt;p&gt;I’m &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;@nyaomaru&lt;/a&gt;, a frontend engineer who recently discovered the deliciousness of a cheese whose name sounds a bit like Al Capone. 🧀&lt;/p&gt;

&lt;p&gt;Are you building personal projects?&lt;/p&gt;

&lt;p&gt;Or contributing to OSS?&lt;/p&gt;

&lt;p&gt;Or maybe you are busy playing &lt;strong&gt;Resident Evil 9 🧟 and CloverPit 🍀&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;Whatever the case, I respect your admirable dedication as an engineer.&lt;/p&gt;

&lt;p&gt;Now, when managing OSS on GitHub, one file that becomes very important is &lt;code&gt;CHANGELOG.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why is it important?&lt;/p&gt;

&lt;p&gt;For example, when a library I use gets updated, the first thing I usually check is &lt;code&gt;CHANGELOG.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Recently, I have been experimenting with an AI triage workflow. For Dependabot updates, I feed release notes and &lt;code&gt;CHANGELOG.md&lt;/code&gt; into AI, then ask Claude Code to evaluate the risk and help with triage.&lt;/p&gt;

&lt;p&gt;This has been very useful. I do not have to check everything manually, and that makes me happy 😺&lt;/p&gt;

&lt;p&gt;In other words, &lt;code&gt;CHANGELOG.md&lt;/code&gt; is becoming a document that is not only read by humans, but also read by AI as input for dependency update decisions.&lt;/p&gt;

&lt;p&gt;So, &lt;code&gt;CHANGELOG.md&lt;/code&gt; will continue to be very useful in the coming era.&lt;/p&gt;

&lt;p&gt;But surprisingly, many projects do not maintain it well.&lt;/p&gt;

&lt;p&gt;For example, even some very useful React development tools sometimes ship silent releases without any release notes at all.&lt;/p&gt;

&lt;p&gt;When that happens, users have to dig through commit history and figure out things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What new features were added?&lt;/li&gt;
&lt;li&gt;What changed?&lt;/li&gt;
&lt;li&gt;Is it safe to upgrade?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, busy engineers do not have time for that. I am not that busy, but if I had that time, I would rather play &lt;strong&gt;CloverPit 🍀&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So, to protect my precious game time, I built an OSS tool that automatically generates &lt;code&gt;CHANGELOG.md&lt;/code&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;
        nyaomaru
      &lt;/a&gt; / &lt;a href="https://github.com/nyaomaru/changelog-bot" rel="noopener noreferrer"&gt;
        changelog-bot
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Automatic create changelog with AI. 🤖 It provides CLI and github actions. 🚀  https://www.npmjs.com/package/&lt;a class="mentioned-user" href="https://dev.to/nyaomaru"&gt;@nyaomaru&lt;/a&gt;/changelog-bot
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/nyaomaru/changelog-bot/main/public/ChangelogBot_logo02.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fchangelog-bot%2Fmain%2Fpublic%2FChangelogBot_logo02.png" alt="changelog-bot type logo" width="360"&gt;&lt;/a&gt;
&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;
    &lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/nyaomaru/changelog-bot/main/public/ChangelogBot_image.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fchangelog-bot%2Fmain%2Fpublic%2FChangelogBot_image.png" width="600px" alt="changelog-bot logo"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Releases should feel exciting, not tedious.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@nyaomaru/changelog-bot&lt;/code&gt; 🤖 turns your Git history and release notes into a polished changelog entry (and optional PR) in a single run. Drop it into CI, run it locally, or hand it to your release captain—either way, you ship with a crisp changelog and zero copy-paste fatigue.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why changelog-bot?&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automated storytelling:&lt;/strong&gt; Combines commit history, PR titles, and release notes to produce human-ready changelog sections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM superpowers (optional):&lt;/strong&gt; Connect OpenAI or Anthropic for tone-aware summaries or skip API keys entirely and rely on a robust heuristic fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR-ready output:&lt;/strong&gt; Can open a pull request with updated changelog, compare links, and release notes already wired up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safe defaults:&lt;/strong&gt; Detects duplicate versions, keeps compare links current, and won’t fail a release if AI is down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI-native:&lt;/strong&gt; Works as a GitHub Action, reusable workflow, or plain CLI—no fragile scripting required.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-alert markdown-alert-important"&gt;
&lt;p class="markdown-alert-title"&gt;Important&lt;/p&gt;
&lt;p&gt;This project is currently in its early stages…&lt;/p&gt;
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nyaomaru/changelog-bot" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;In this article, I will walk through the changes from v0.1 and what I am planning next.&lt;/p&gt;

&lt;p&gt;Let’s take a look!&lt;/p&gt;




&lt;h2&gt;
  
  
  🤖 What is changelog-bot?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/changelog-bot" rel="noopener noreferrer"&gt;&lt;code&gt;changelog-bot&lt;/code&gt;&lt;/a&gt; is a CLI and GitHub Action that automatically creates a Pull Request to update &lt;code&gt;CHANGELOG.md&lt;/code&gt; based on Git history, Pull Requests, and GitHub Release Notes.&lt;/p&gt;

&lt;p&gt;What it does is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collect commits and PRs from the previous release to the current release&lt;/li&gt;
&lt;li&gt;Categorize changes into sections such as &lt;code&gt;Added&lt;/code&gt;, &lt;code&gt;Changed&lt;/code&gt;, &lt;code&gt;Fixed&lt;/code&gt;, &lt;code&gt;Docs&lt;/code&gt;, and &lt;code&gt;Chore&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add a new release section to &lt;code&gt;CHANGELOG.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Create a Pull Request with that change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you configure an AI provider, it can use &lt;code&gt;OpenAI&lt;/code&gt;, &lt;code&gt;Anthropic&lt;/code&gt;, or &lt;code&gt;Gemini&lt;/code&gt; to generate a more readable changelog section.&lt;/p&gt;

&lt;p&gt;On the other hand, it also works with a fallback mode when no API key is provided.&lt;/p&gt;

&lt;p&gt;So it is totally fine even if you do not use AI.&lt;/p&gt;

&lt;p&gt;In other words, this is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A tool that can do nothing without AI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is more like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An automation tool that works better with AI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the key idea.&lt;/p&gt;




&lt;h2&gt;
  
  
  🐾 What was available up to v0.1
&lt;/h2&gt;

&lt;p&gt;Up to v0.1, I focused on building the foundation of changelog-bot.&lt;/p&gt;

&lt;p&gt;It already supported:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CLI and GitHub Action usage&lt;/li&gt;
&lt;li&gt;Changelog generation from Git history&lt;/li&gt;
&lt;li&gt;Importing GitHub Release Notes&lt;/li&gt;
&lt;li&gt;OpenAI and Anthropic providers&lt;/li&gt;
&lt;li&gt;Heuristic fallback&lt;/li&gt;
&lt;li&gt;Dry-run mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, the basic experience was already there:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Automatically update &lt;code&gt;CHANGELOG.md&lt;/code&gt; during a release workflow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I also introduced it in this earlier article:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/nyaomaru/generate-changelogmd-automatically-26g1" class="crayons-story__hidden-navigation-link"&gt;Generate CHANGELOG.md Automatically 🤖&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/nyaomaru" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1436158%2Fa78bc4ed-7796-4760-baa5-06283c2404d6.png" alt="nyaomaru profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/nyaomaru" class="crayons-story__secondary fw-medium m:hidden"&gt;
              nyaomaru
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                nyaomaru
                
              
              &lt;div id="story-author-preview-content-3072785" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/nyaomaru" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1436158%2Fa78bc4ed-7796-4760-baa5-06283c2404d6.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;nyaomaru&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/nyaomaru/generate-changelogmd-automatically-26g1" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Nov 30 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/nyaomaru/generate-changelogmd-automatically-26g1" id="article-link-3072785"&gt;
          Generate CHANGELOG.md Automatically 🤖
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/typescript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;typescript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ci"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ci&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/nyaomaru/generate-changelogmd-automatically-26g1" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;14&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/nyaomaru/generate-changelogmd-automatically-26g1#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              3&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;But there were still some problems.&lt;/p&gt;

&lt;p&gt;In particular, even though users could choose an AI provider, there was still no strong way to adjust the language or writing style of the generated &lt;code&gt;CHANGELOG.md&lt;/code&gt; for each project.&lt;/p&gt;

&lt;p&gt;For example, there was no good way to pass project-specific rules such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Write it in Japanese.”&lt;/li&gt;
&lt;li&gt;“Group internal changes under Chore.”&lt;/li&gt;
&lt;li&gt;“Group dependency updates together.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the customization experience was still weak.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 v0.2: Gemini provider support
&lt;/h2&gt;

&lt;p&gt;In v0.2, I added &lt;strong&gt;Gemini&lt;/strong&gt; as a new LLM provider.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzcjt2918edvubhn3xz4c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzcjt2918edvubhn3xz4c.png" alt="Changelog-bot power up"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now changelog-bot supports these three providers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;OpenAI&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Anthropic&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Gemini&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can specify Gemini from the CLI like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;changelog-bot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--provider&lt;/span&gt; gemini &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--release-tag&lt;/span&gt; HEAD &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--release-name&lt;/span&gt; 0.2.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You can also use it from GitHub Actions:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemini&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GEMINI_API_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I also added &lt;strong&gt;dry-run diagnostics&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Provider: gemini
Model: gemini-3.5-flash
AI used: &lt;span class="nb"&gt;true
&lt;/span&gt;Fallback reasons: none
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This makes it easier to check whether the AI provider was actually used, or whether changelog-bot fell back to the heuristic mode.&lt;/p&gt;

&lt;p&gt;It is a small feature, but I think this kind of observability is very important for CI tools.&lt;/p&gt;

&lt;p&gt;When something runs in CI, you want to quickly understand what actually happened.&lt;/p&gt;


&lt;h2&gt;
  
  
  🚀 v0.3: Changelog customization
&lt;/h2&gt;

&lt;p&gt;In v0.3, I added customization support for changelog generation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa126nq69tgqofz9k1uux.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa126nq69tgqofz9k1uux.png" alt="Changelog-bot note"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The newly added options are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--language&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Specifies the language of the generated &lt;code&gt;CHANGELOG.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--instructions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Passes inline instructions to the AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--instructions-file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Passes instructions to the AI from a file&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example, if you want to generate a changelog in Dutch, you can write:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;changelog-bot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--provider&lt;/span&gt; gemini &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--release-tag&lt;/span&gt; HEAD &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--release-name&lt;/span&gt; 0.3.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--language&lt;/span&gt; &lt;span class="nb"&gt;nl&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instructions&lt;/span&gt; &lt;span class="s2"&gt;"Schrijf kort en duidelijk in het Nederlands. Groepeer interne wijzigingen onder Chore."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You can also define project-specific rules in a file.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;changelog-bot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--provider&lt;/span&gt; openai &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--release-tag&lt;/span&gt; HEAD &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--release-name&lt;/span&gt; 0.3.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--language&lt;/span&gt; &lt;span class="nb"&gt;nl&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instructions-file&lt;/span&gt; .github/changelog-instructions.md &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For example, &lt;code&gt;.github/changelog-instructions.md&lt;/code&gt; can contain rules like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Write concise release notes in Dutch.

Rules:
&lt;span class="p"&gt;
-&lt;/span&gt; Focus on user-facing changes.
&lt;span class="p"&gt;-&lt;/span&gt; Group internal refactors under Chore.
&lt;span class="p"&gt;-&lt;/span&gt; Group dependency updates together.
&lt;span class="p"&gt;-&lt;/span&gt; Avoid mentioning commit hashes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With this, changelog-bot is no longer just “a tool that generates changelogs with AI.”&lt;/p&gt;

&lt;p&gt;It is moving toward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A tool that can generate a &lt;code&gt;CHANGELOG.md&lt;/code&gt; that feels right for each project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Also, when customization is enabled, changelog-bot does not simply use GitHub Release Notes as-is, even if they exist.&lt;/p&gt;

&lt;p&gt;Instead, it passes them to the model and lets the model rewrite them.&lt;/p&gt;

&lt;p&gt;This means GitHub Release Notes can still be used as source material, while the final output reflects the specified &lt;code&gt;language&lt;/code&gt; and &lt;code&gt;instructions&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  🥌 Roadmap toward v1
&lt;/h2&gt;

&lt;p&gt;With customization added in v0.3, the shape of v1 is becoming much clearer.&lt;/p&gt;

&lt;p&gt;Here are the main things I want to add before v1.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Config file support
&lt;/h3&gt;

&lt;p&gt;Passing CLI options every time is annoying, so I want to support config files.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gemini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instructionsFile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".github/changelog-instructions.md"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The goal is to make the same configuration usable from the CLI, GitHub Actions, and reusable workflows.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Choosing behavior when an AI provider fails
&lt;/h3&gt;

&lt;p&gt;Right now, changelog-bot falls back when an AI provider fails.&lt;/p&gt;

&lt;p&gt;This is convenient, but some projects may want the workflow to fail if AI generation fails.&lt;/p&gt;

&lt;p&gt;So I am considering options like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--fail-on-llm-error&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--require-provider&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--no-ai&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since changelog-bot is intended to be used in CI, users should be able to choose between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Fail when AI generation fails”&lt;/li&gt;
&lt;li&gt;“Fallback and continue”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both behaviors are valid depending on the project.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. JSON report output
&lt;/h3&gt;

&lt;p&gt;Dry-run diagnostics already exist, but I also want to add a JSON report.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;changelog-bot &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nt"&gt;--report-json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For example, it would be useful to output machine-readable information such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;provider&lt;/li&gt;
&lt;li&gt;model&lt;/li&gt;
&lt;li&gt;aiUsed&lt;/li&gt;
&lt;li&gt;fallbackReasons&lt;/li&gt;
&lt;li&gt;generated sections&lt;/li&gt;
&lt;li&gt;PR title&lt;/li&gt;
&lt;li&gt;labels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This should make it easier to integrate changelog-bot with other CI tools and bots.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Better changelog quality
&lt;/h3&gt;

&lt;p&gt;I also want to keep improving the quality of the generated changelog.&lt;/p&gt;

&lt;p&gt;Some areas I want to improve are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Grouping dependency updates&lt;/li&gt;
&lt;li&gt;Detecting breaking changes&lt;/li&gt;
&lt;li&gt;Label-based include / exclude rules&lt;/li&gt;
&lt;li&gt;Custom section names&lt;/li&gt;
&lt;li&gt;Better duplicate detection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Especially in OSS, what should be included in a changelog differs from project to project.&lt;/p&gt;

&lt;p&gt;I want changelog-bot to handle those differences better.&lt;/p&gt;


&lt;h2&gt;
  
  
  🎯 Summary
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CHANGELOG.md&lt;/code&gt; is a very plain file, but for users, it is an important document.&lt;/p&gt;

&lt;p&gt;Recently, it is also becoming more common to let AI read it and use it as input for dependency update decisions.&lt;/p&gt;

&lt;p&gt;That is why &lt;code&gt;CHANGELOG.md&lt;/code&gt; should not just be generated somehow.&lt;/p&gt;

&lt;p&gt;It should be generated in a readable way that fits each project’s context.&lt;/p&gt;

&lt;p&gt;But writing it carefully by hand every time is honestly a pain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/changelog-bot" rel="noopener noreferrer"&gt;&lt;code&gt;changelog-bot&lt;/code&gt;&lt;/a&gt; is a tool for automating that process with Git history, Pull Requests, GitHub Release Notes, and &lt;strong&gt;AI&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In v0.2, I added Gemini support.&lt;/p&gt;

&lt;p&gt;In v0.3, I added customization through &lt;code&gt;language&lt;/code&gt;, &lt;code&gt;instructions&lt;/code&gt;, and &lt;code&gt;instructions file&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With these changes, changelog-bot is moving from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Just generating a changelog automatically&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Generating a changelog that fits each project&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;v1 is getting closer.&lt;/p&gt;

&lt;p&gt;If you are interested, please give the repository a ⭐ and watch over it warmly!&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;
        nyaomaru
      &lt;/a&gt; / &lt;a href="https://github.com/nyaomaru/changelog-bot" rel="noopener noreferrer"&gt;
        changelog-bot
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Automatic create changelog with AI. 🤖 It provides CLI and github actions. 🚀  https://www.npmjs.com/package/&lt;a class="mentioned-user" href="https://dev.to/nyaomaru"&gt;@nyaomaru&lt;/a&gt;/changelog-bot
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/nyaomaru/changelog-bot/main/public/ChangelogBot_logo02.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fchangelog-bot%2Fmain%2Fpublic%2FChangelogBot_logo02.png" alt="changelog-bot type logo" width="360"&gt;&lt;/a&gt;
&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;
    &lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/nyaomaru/changelog-bot/main/public/ChangelogBot_image.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fchangelog-bot%2Fmain%2Fpublic%2FChangelogBot_image.png" width="600px" alt="changelog-bot logo"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Releases should feel exciting, not tedious.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@nyaomaru/changelog-bot&lt;/code&gt; 🤖 turns your Git history and release notes into a polished changelog entry (and optional PR) in a single run. Drop it into CI, run it locally, or hand it to your release captain—either way, you ship with a crisp changelog and zero copy-paste fatigue.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why changelog-bot?&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automated storytelling:&lt;/strong&gt; Combines commit history, PR titles, and release notes to produce human-ready changelog sections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM superpowers (optional):&lt;/strong&gt; Connect OpenAI or Anthropic for tone-aware summaries or skip API keys entirely and rely on a robust heuristic fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR-ready output:&lt;/strong&gt; Can open a pull request with updated changelog, compare links, and release notes already wired up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safe defaults:&lt;/strong&gt; Detects duplicate versions, keeps compare links current, and won’t fail a release if AI is down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI-native:&lt;/strong&gt; Works as a GitHub Action, reusable workflow, or plain CLI—no fragile scripting required.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-alert markdown-alert-important"&gt;
&lt;p class="markdown-alert-title"&gt;Important&lt;/p&gt;
&lt;p&gt;This project is currently in its early stages…&lt;/p&gt;
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nyaomaru/changelog-bot" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;Now let’s auto-generate our &lt;code&gt;CHANGELOG.md&lt;/code&gt; files and use the saved time to play some games! 🧟🍀&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>opensource</category>
      <category>changelog</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Which OpenAPI Codegen Should You Choose? openapi-typescript vs hey-api vs Orval vs Kubb</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Wed, 20 May 2026 13:34:57 +0000</pubDate>
      <link>https://dev.to/nyaomaru/which-openapi-codegen-should-you-choose-openapi-typescript-vs-hey-api-vs-orval-vs-kubb-100p</link>
      <guid>https://dev.to/nyaomaru/which-openapi-codegen-should-you-choose-openapi-typescript-vs-hey-api-vs-orval-vs-kubb-100p</guid>
      <description>&lt;p&gt;Hoi hoi!&lt;/p&gt;

&lt;p&gt;I'm &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;@nyaomaru&lt;/a&gt;, a frontend engineer who thought OpenAPI codegen would make API clients easier, but somehow ended up lost in a forest of generated files. 🌳🌳🌳&lt;/p&gt;

&lt;p&gt;Writing API clients by hand is painful.&lt;/p&gt;

&lt;p&gt;Usually, we want things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generated TypeScript types&lt;/li&gt;
&lt;li&gt;generated request functions&lt;/li&gt;
&lt;li&gt;maybe &lt;code&gt;TanStack Query&lt;/code&gt; hooks&lt;/li&gt;
&lt;li&gt;maybe &lt;code&gt;MSW&lt;/code&gt; mocks&lt;/li&gt;
&lt;li&gt;maybe even &lt;code&gt;Zod&lt;/code&gt; schemas&lt;/li&gt;
&lt;li&gt;and, if possible, everything growing automatically from OpenAPI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is where OpenAPI codegen tools are useful.&lt;/p&gt;

&lt;p&gt;But after comparing several tools with a fairly large real-world schema, I realized something important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAPI codegen is not only about what a tool can generate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The generated output itself also matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how many files it creates&lt;/li&gt;
&lt;li&gt;how fast generation is&lt;/li&gt;
&lt;li&gt;how friendly it is to linting&lt;/li&gt;
&lt;li&gt;how easy it is for the IDE to index&lt;/li&gt;
&lt;li&gt;how errors are modeled&lt;/li&gt;
&lt;li&gt;how authentication is handled&lt;/li&gt;
&lt;li&gt;how much generated code your team is willing to maintain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, I compared the following four tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Version tested&lt;/th&gt;
&lt;th&gt;Main setup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v7.13.0&lt;/td&gt;
&lt;td&gt;types only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@hey-api/openapi-ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v0.96.1&lt;/td&gt;
&lt;td&gt;types + SDK + fetch client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v8.8.1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mode: "tags-split"&lt;/code&gt; / &lt;code&gt;client: "fetch"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v4.37.2&lt;/td&gt;
&lt;td&gt;client / types / schemas generated by plugins&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frr61d7ozn3dwxht4s5jo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frr61d7ozn3dwxht4s5jo.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The OpenAPI schema used for this comparison was roughly this size. (It was generated by &lt;code&gt;drf-spectacular&lt;/code&gt;)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;openapi.yaml&lt;/code&gt; lines&lt;/td&gt;
&lt;td&gt;about 75,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;openapi.yaml&lt;/code&gt; size&lt;/td&gt;
&lt;td&gt;about 2 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;operations&lt;/td&gt;
&lt;td&gt;about 1,200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Some endpoint names, schema names, type names, and directory names in this article are simplified or replaced for publication. Also, these libraries move fast, so the generated output and issues described here are based on the versions tested at the time of writing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a small API, honestly, any of these tools may work fine.&lt;/p&gt;

&lt;p&gt;But at this scale, differences become very visible in generation speed, file count, lint behavior, enum handling, error models, and day-to-day maintainability.&lt;/p&gt;

&lt;p&gt;Let's walk through the comparison.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 TL;DR
&lt;/h2&gt;

&lt;p&gt;My conclusion is this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Tool I would choose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You only need TypeScript types&lt;/td&gt;
&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want operationId-based SDK functions and interceptors&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@hey-api/openapi-ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want &lt;code&gt;TanStack Query&lt;/code&gt;, &lt;code&gt;Zod&lt;/code&gt;, &lt;code&gt;MSW&lt;/code&gt;, or &lt;code&gt;Faker&lt;/code&gt; generated from OpenAPI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You value 1 operation = 1 file and plugin-based codegen architecture&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For my use case, &lt;strong&gt;hey-api felt the most practical&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The reasons were simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it generates functions from &lt;code&gt;operationId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;arguments are consistently shaped like &lt;code&gt;{ query, path, body }&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;it has a result-style error model&lt;/li&gt;
&lt;li&gt;it supports axios-like interceptors&lt;/li&gt;
&lt;li&gt;the generated output is much lighter than Orval or Kubb&lt;/li&gt;
&lt;li&gt;it still leaves room for &lt;code&gt;TanStack Query&lt;/code&gt; and &lt;code&gt;Zod&lt;/code&gt; plugins later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That said, if your project already uses &lt;code&gt;openapi-typescript + openapi-fetch&lt;/code&gt; and you are happy with it, I do not think you need to migrate immediately.&lt;/p&gt;

&lt;p&gt;The difference is not huge enough to justify migration by itself.&lt;/p&gt;

&lt;p&gt;I would consider switching only when you start feeling pain around SDK functions, interceptors, result handling, mocks, generated hooks, or schema-driven tooling.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In this article, I will call &lt;code&gt;@hey-api/openapi-ts&lt;/code&gt; simply &lt;code&gt;hey-api&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🌓 Two families of OpenAPI codegen tools
&lt;/h2&gt;

&lt;p&gt;Before comparing each tool one by one, I think it is easier to divide them into two families.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;Family A: thin output, client-focused&lt;/th&gt;
&lt;th&gt;Family B: SDK-first, ecosystem generation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Representative tools&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;openapi-typescript&lt;/code&gt; / &lt;code&gt;hey-api&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Orval&lt;/code&gt; / &lt;code&gt;Kubb&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main purpose&lt;/td&gt;
&lt;td&gt;accurate types and lightweight runtime&lt;/td&gt;
&lt;td&gt;generated SDK + hooks + mocks + validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error model&lt;/td&gt;
&lt;td&gt;result-style &lt;code&gt;{ data, error, response }&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;wrapper / throw / client-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output size&lt;/td&gt;
&lt;td&gt;small: types only or compact SDK&lt;/td&gt;
&lt;td&gt;large: tag-level or operation-level files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ecosystem features&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TanStack Query&lt;/code&gt; / &lt;code&gt;Zod&lt;/code&gt; depending on tool&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TanStack Query&lt;/code&gt; / &lt;code&gt;Zod&lt;/code&gt; / &lt;code&gt;MSW&lt;/code&gt; / &lt;code&gt;Faker&lt;/code&gt; / &lt;code&gt;SWR&lt;/code&gt; etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configuration style&lt;/td&gt;
&lt;td&gt;minimal&lt;/td&gt;
&lt;td&gt;codegen platform&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This distinction is important.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;openapi-typescript&lt;/code&gt; and &lt;code&gt;Kubb&lt;/code&gt; are both OpenAPI codegen tools, but they are not trying to solve the same problem.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;openapi-typescript&lt;/code&gt; mainly focuses on generating accurate TypeScript types.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Kubb&lt;/code&gt;, on the other hand, is closer to a codegen platform that can generate clients, types, schemas, mocks, and other assets in a structured way.&lt;/p&gt;

&lt;p&gt;Comparing them directly is a bit like comparing a kitchen knife with a full kitchen. 🧑‍🍳🔪&lt;/p&gt;

&lt;p&gt;Both are related to cooking, but they are not the same kind of thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏎️ Generation speed
&lt;/h2&gt;

&lt;p&gt;First, generation speed.&lt;/p&gt;

&lt;p&gt;I measured each command three times on &lt;code&gt;WSL2&lt;/code&gt; / &lt;code&gt;Node v24.14&lt;/code&gt; using &lt;code&gt;npx &amp;lt;cmd&amp;gt;&lt;/code&gt;. So the numbers include &lt;code&gt;npx&lt;/code&gt; startup overhead. (a custom-built PC)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;1st run&lt;/th&gt;
&lt;th&gt;2nd run&lt;/th&gt;
&lt;th&gt;3rd run&lt;/th&gt;
&lt;th&gt;Average&lt;/th&gt;
&lt;th&gt;Max RSS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.66 s&lt;/td&gt;
&lt;td&gt;1.50 s&lt;/td&gt;
&lt;td&gt;1.40 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;about 1.5 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;about 335 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@hey-api/openapi-ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8.74 s&lt;/td&gt;
&lt;td&gt;8.84 s&lt;/td&gt;
&lt;td&gt;6.43 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;about 8.0 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;about 495 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6.01 s&lt;/td&gt;
&lt;td&gt;6.12 s&lt;/td&gt;
&lt;td&gt;4.48 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;about 5.5 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;about 415 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;17.29 s&lt;/td&gt;
&lt;td&gt;19.82 s&lt;/td&gt;
&lt;td&gt;17.13 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;about 18.1 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;about 880 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Relative to &lt;code&gt;openapi-typescript&lt;/code&gt;, the difference looked like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Relative speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;about 3.7x slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hey-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;about 5.3x slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;about 12x slower&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why openapi-typescript is so fast
&lt;/h3&gt;

&lt;p&gt;In this setup, &lt;code&gt;openapi-typescript&lt;/code&gt; only generates types into a single file.&lt;/p&gt;

&lt;p&gt;The workflow is basically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;read the schema&lt;/li&gt;
&lt;li&gt;convert it into TypeScript types&lt;/li&gt;
&lt;li&gt;write one file&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That means very little file I/O.&lt;/p&gt;

&lt;p&gt;The generated output was just &lt;code&gt;types.gen.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At about 1.5 seconds, it is fast enough that running it in CI, pre-commit, or during local development barely feels like a problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Kubb is slower
&lt;/h3&gt;

&lt;p&gt;Kubb has a different philosophy.&lt;/p&gt;

&lt;p&gt;It splits things much more aggressively.&lt;/p&gt;

&lt;p&gt;In this test, I generated operation-level clients, operation-level types, and schemas.&lt;/p&gt;

&lt;p&gt;So the cost is not only computation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The number of files written to disk becomes a major factor.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The structure is clean from a bundling and organization perspective, but with around 1,200 operations, the file system cost becomes noticeable.&lt;/p&gt;

&lt;p&gt;For this kind of schema, I would not treat Kubb as something I casually run on every small change unless the generation target is narrowed.&lt;/p&gt;

&lt;p&gt;It feels more suitable for a workflow where generated files are committed, generated in CI, or carefully scoped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orval was faster than I expected
&lt;/h3&gt;

&lt;p&gt;I expected Orval to be slower.&lt;/p&gt;

&lt;p&gt;It can generate things like &lt;code&gt;TanStack Query&lt;/code&gt;, &lt;code&gt;Zod&lt;/code&gt;, and &lt;code&gt;MSW&lt;/code&gt;, so I assumed it would be heavier.&lt;/p&gt;

&lt;p&gt;But with &lt;code&gt;tags-split&lt;/code&gt;, the main functions are grouped by tag.&lt;/p&gt;

&lt;p&gt;In my schema, there were about 25 tags.&lt;/p&gt;

&lt;p&gt;So Orval was not generating one giant file, but it also was not generating one file per operation.&lt;/p&gt;

&lt;p&gt;That middle ground worked well for speed.&lt;/p&gt;

&lt;p&gt;However, this does not mean the output was small.&lt;/p&gt;

&lt;p&gt;It still generated more than 2,600 schema files.&lt;/p&gt;




&lt;h2&gt;
  
  
  🗺️ Output size
&lt;/h2&gt;

&lt;p&gt;Next, generated output size and file count.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;File count&lt;/th&gt;
&lt;th&gt;Total size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/api/types.gen.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2.4 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@hey-api/openapi-ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/api/hey-api/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;2.9 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/api/orval/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2,719&lt;/td&gt;
&lt;td&gt;14 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/api/kubb/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3,877&lt;/td&gt;
&lt;td&gt;24 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Just from these numbers, the tools clearly live in different worlds.&lt;/p&gt;

&lt;p&gt;My rough feeling was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;openapi-typescript&lt;/code&gt;: one house 🏠&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hey-api&lt;/code&gt;: a small town 🏙️&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Orval&lt;/code&gt;: a small forest 🌲🌲&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Kubb&lt;/code&gt;: a deep forest 🌳🌳🌳&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;BTW, I love rural life so forest is better. 🌳&lt;/p&gt;

&lt;h3&gt;
  
  
  openapi-typescript
&lt;/h3&gt;

&lt;p&gt;The output was only &lt;code&gt;types.gen.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It contained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;paths&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;components&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;operations&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One file is simple and easy to understand.&lt;/p&gt;

&lt;p&gt;But a 2.4 MB TypeScript file can still be uncomfortable for IDE indexing.&lt;/p&gt;

&lt;p&gt;So even though the file count is tiny, the single file itself is large.&lt;/p&gt;

&lt;h3&gt;
  
  
  hey-api
&lt;/h3&gt;

&lt;p&gt;hey-api generated 16 files.&lt;/p&gt;

&lt;p&gt;The main files looked like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;types.gen.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;request / response types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sdk.gen.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;generated operation functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;index.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;barrel exports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;client.gen.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;default client instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;client/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fetch client implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;core/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;auth, serializers, utils, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;types.gen.ts&lt;/code&gt; was about 1.7 MB, and &lt;code&gt;sdk.gen.ts&lt;/code&gt; was about 750 KB.&lt;/p&gt;

&lt;p&gt;With 1,200 operations, the SDK file naturally becomes large.&lt;/p&gt;

&lt;p&gt;But compared with Orval and Kubb, the output still felt manageable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orval
&lt;/h3&gt;

&lt;p&gt;I used Orval with &lt;code&gt;tags-split&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The structure looked roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/api/orval/
├─ schemas/
│  ├─ ordersWriteRequest.ts
│  ├─ ordersListParams.ts
│  └─ ...
├─ order/
│  └─ order.ts
├─ product/
│  └─ product.ts
└─ ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Functions were grouped by tag.&lt;/p&gt;

&lt;p&gt;In this schema:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tag files: about 25&lt;/li&gt;
&lt;li&gt;schema files: more than 2,600&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The largest generated file was around 30,000 lines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kubb
&lt;/h3&gt;

&lt;p&gt;Kubb generated an even more split structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/api/kubb/
├─ types/
├─ clients/
├─ schemas/
└─ index.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Directory&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;types/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;operation-level types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clients/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;operation-level functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;schemas/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;individual schemas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;index.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;barrel exports&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The nice part is that the structure is close to 1 operation = 1 file.&lt;/p&gt;

&lt;p&gt;That makes grep easy and can be friendly to tree-shaking.&lt;/p&gt;

&lt;p&gt;But the tradeoff is real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;large PR diffs&lt;/li&gt;
&lt;li&gt;heavier IDE indexing&lt;/li&gt;
&lt;li&gt;slower clone and checkout&lt;/li&gt;
&lt;li&gt;more generated files to commit or ignore&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧪 Calling the same endpoint
&lt;/h2&gt;

&lt;p&gt;Now let's compare the actual call sites.&lt;/p&gt;

&lt;p&gt;Assume this endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET /orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It accepts common list query parameters like &lt;code&gt;page&lt;/code&gt; and &lt;code&gt;page_size&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  openapi-typescript
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;openapi-typescript&lt;/code&gt; only generates types, so I used it with &lt;code&gt;openapi-fetch&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openapi-fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;paths&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./api/types.gen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:8000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is that the endpoint is specified as a path string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is simple and readable.&lt;/p&gt;

&lt;p&gt;IDE autocomplete also works well.&lt;/p&gt;

&lt;p&gt;But it is not operationId-based, so if the endpoint changes, rename refactoring is not as direct as function-based clients.&lt;/p&gt;

&lt;h3&gt;
  
  
  hey-api
&lt;/h3&gt;

&lt;p&gt;hey-api generates SDK functions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ordersList&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./api/hey-api/sdk.gen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ordersList&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function name comes from &lt;code&gt;operationId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I like this because you can grep by function name and use IDE rename refactoring more naturally.&lt;/p&gt;

&lt;p&gt;The argument shape is also consistent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This consistency becomes useful later when building wrappers, logging, retries, auth handling, or tracing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orval
&lt;/h3&gt;

&lt;p&gt;For Orval, I used &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;tags-split&lt;/code&gt;, and &lt;code&gt;useNamedParameters: true&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ordersList&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./api/orval/orders/order&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ordersList&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The return value is like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, this is easy to use.&lt;/p&gt;

&lt;p&gt;But with the default fetch client, 4xx and 5xx responses are not thrown as errors.&lt;/p&gt;

&lt;p&gt;That point is very important in real applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kubb
&lt;/h3&gt;

&lt;p&gt;Kubb generates operation-level functions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ordersList&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./api/kubb/clients/ordersList&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ordersList&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On success, it returns the response data directly.&lt;/p&gt;

&lt;p&gt;So the call site is very short.&lt;/p&gt;

&lt;p&gt;However, if you want to access response headers or status, the default return shape may not be enough.&lt;/p&gt;

&lt;p&gt;Also, path parameters and body arguments can become positional depending on the endpoint, which can make generic wrappers harder to design.&lt;/p&gt;

&lt;h2&gt;
  
  
  🖇️ Path params and body arguments
&lt;/h2&gt;

&lt;p&gt;A list endpoint does not show all differences, so let's compare path params and body arguments too.&lt;/p&gt;

&lt;h3&gt;
  
  
  GET by id
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// openapi-typescript&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/orders/{id}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hey-api&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ordersRetrieve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Orval&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ordersRetrieve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Kubb&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ordersRetrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Summary:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Path param style&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;params.path&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hey-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ path: { id } }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ id }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;positional argument like &lt;code&gt;"123"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Kubb is short.&lt;/p&gt;

&lt;p&gt;But if an endpoint has multiple path params, positional arguments grow like &lt;code&gt;(id1, id2, ...)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is partly a preference issue, but for large API clients I usually prefer named object arguments.&lt;/p&gt;

&lt;h3&gt;
  
  
  POST body
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// openapi-typescript&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hey-api&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ordersCreate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Orval&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ordersCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Kubb&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ordersCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Orval and Kubb feel natural when the body is the first argument.&lt;/p&gt;

&lt;p&gt;But for POST endpoints without a body, this can sometimes lead to explicit &lt;code&gt;undefined&lt;/code&gt; arguments.&lt;/p&gt;

&lt;p&gt;hey-api and openapi-typescript add one level with &lt;code&gt;{ body }&lt;/code&gt;, but the shape remains consistent with &lt;code&gt;query&lt;/code&gt; and &lt;code&gt;path&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For a large application, I personally prefer the consistency of hey-api.&lt;/p&gt;




&lt;h2&gt;
  
  
  💭 Function signature philosophy
&lt;/h2&gt;

&lt;p&gt;Here is the broader comparison of call signatures.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;hey-api&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Orval&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;endpoint selection&lt;/td&gt;
&lt;td&gt;URL string&lt;/td&gt;
&lt;td&gt;operationId function&lt;/td&gt;
&lt;td&gt;operationId function&lt;/td&gt;
&lt;td&gt;operationId function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;query&lt;/td&gt;
&lt;td&gt;&lt;code&gt;params.query&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ query }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;first argument&lt;/td&gt;
&lt;td&gt;first argument&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;path&lt;/td&gt;
&lt;td&gt;&lt;code&gt;params.path&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ path }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{ id }&lt;/code&gt; or positional&lt;/td&gt;
&lt;td&gt;positional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;body&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ body }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ body }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;first argument&lt;/td&gt;
&lt;td&gt;first argument&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;headers&lt;/td&gt;
&lt;td&gt;options / middleware&lt;/td&gt;
&lt;td&gt;options / interceptor&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RequestInit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cross-cutting behavior&lt;/td&gt;
&lt;td&gt;middleware&lt;/td&gt;
&lt;td&gt;interceptor&lt;/td&gt;
&lt;td&gt;mutator&lt;/td&gt;
&lt;td&gt;config / custom client&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key strength of hey-api is that every endpoint moves toward the same shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;api&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes cross-cutting behavior easier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;logging&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;trace ids&lt;/li&gt;
&lt;li&gt;request ids&lt;/li&gt;
&lt;li&gt;custom headers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Orval and Kubb often produce shorter call sites, but signatures can vary more by endpoint.&lt;/p&gt;

&lt;p&gt;In a small codebase, that is probably fine.&lt;/p&gt;

&lt;p&gt;In a 1,200-operation API, consistency matters a lot more.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚠️ Error model
&lt;/h2&gt;

&lt;p&gt;This was one of the most important differences.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Default behavior&lt;/th&gt;
&lt;th&gt;Can be changed?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;openapi-typescript&lt;/code&gt; + &lt;code&gt;openapi-fetch&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{ data, error, response }&lt;/code&gt; result union&lt;/td&gt;
&lt;td&gt;can throw via middleware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hey-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{ data, error, response }&lt;/code&gt; result union&lt;/td&gt;
&lt;td&gt;can throw with &lt;code&gt;throwOnError: true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wrapper like &lt;code&gt;{ data, status, headers }&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;can throw with custom fetch / mutator / config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;success returns &lt;code&gt;data&lt;/code&gt;, failure throws&lt;/td&gt;
&lt;td&gt;customizable via client&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Result union in openapi-typescript and hey-api
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;openapi-fetch&lt;/code&gt; and hey-api, non-2xx responses can be represented in &lt;code&gt;error&lt;/code&gt; if the OpenAPI schema defines them.&lt;/p&gt;

&lt;p&gt;But in my &lt;code&gt;drf-spectacular&lt;/code&gt; schema, most non-2xx responses were not described.&lt;/p&gt;

&lt;p&gt;So in practice, &lt;code&gt;error&lt;/code&gt; was often close to &lt;code&gt;never&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That means I still need a runtime check like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With hey-api, I can also opt into throwing behavior per call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ordersList&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;throwOnError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I like having both options.&lt;/p&gt;

&lt;p&gt;Sometimes I want a result-style response.&lt;/p&gt;

&lt;p&gt;Sometimes I want HTTP errors to throw immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orval's default fetch behavior needs attention
&lt;/h3&gt;

&lt;p&gt;With Orval's default fetch client, 4xx and 5xx responses do not throw automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ordersList&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the team forgets to check &lt;code&gt;status&lt;/code&gt;, it is possible to treat an error response as if it were success data.&lt;/p&gt;

&lt;p&gt;For example, the code may try to access &lt;code&gt;data.results&lt;/code&gt; and then fail at runtime because the actual response body was an error shape.&lt;/p&gt;

&lt;p&gt;This is easy to miss if you only look at TypeScript types.&lt;/p&gt;

&lt;p&gt;If I used Orval in production, I would choose one of these patterns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;always check &lt;code&gt;status&lt;/code&gt; at the call site&lt;/li&gt;
&lt;li&gt;configure the fetch client with &lt;code&gt;override.fetch.forceSuccessResponse: true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;override.mutator&lt;/code&gt; and write a custom fetch wrapper for auth, refresh, and errors&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A custom fetch may look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// custom-fetch.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customFetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RequestInit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Orval is powerful, but this behavior should be made explicit as a team rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kubb's throw model
&lt;/h3&gt;

&lt;p&gt;Kubb is closer to a throw-based model by default.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ordersList&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On success, you get the data directly.&lt;/p&gt;

&lt;p&gt;On failure, it throws.&lt;/p&gt;

&lt;p&gt;This is pleasant in UI code.&lt;/p&gt;

&lt;p&gt;But if you need response metadata, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Retry-After&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ETag&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;rate limit headers&lt;/li&gt;
&lt;li&gt;pagination headers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then the default return value may not be enough, and you may need a custom client or wrapper.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Authentication and interceptors
&lt;/h2&gt;

&lt;p&gt;Authentication handling also differs quite a bit.&lt;/p&gt;

&lt;h3&gt;
  
  
  openapi-fetch
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;openapi-fetch&lt;/code&gt; supports middleware.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;onRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you already use &lt;code&gt;openapi-typescript + openapi-fetch&lt;/code&gt;, this is clean and lightweight.&lt;/p&gt;

&lt;h3&gt;
  
  
  hey-api
&lt;/h3&gt;

&lt;p&gt;hey-api has axios-like interceptors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interceptors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This API feels familiar.&lt;/p&gt;

&lt;p&gt;For common headers, logging, trace ids, and 401 refresh flows, this style is easy to work with.&lt;/p&gt;

&lt;p&gt;Generated hey-api functions can also include &lt;code&gt;security&lt;/code&gt; metadata.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ordersList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ThrowOnError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Options&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ordersListData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ThrowOnError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ordersListResponses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ThrowOnError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bearer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a little more to understand at first, but useful for authenticated APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orval
&lt;/h3&gt;

&lt;p&gt;Orval is closer to raw fetch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ordersList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can pass headers per call.&lt;/p&gt;

&lt;p&gt;But in a real app, I would usually create a custom fetch with &lt;code&gt;override.mutator&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kubb
&lt;/h3&gt;

&lt;p&gt;Kubb can configure the client or accept per-call config.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:8000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Headers can also be passed per call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ordersList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But it does not feel like axios-style interceptors are the default mental model.&lt;/p&gt;

&lt;p&gt;For things like 401 refresh, I would expect to wire my own client behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  🪛 TanStack Query, Zod, MSW, and Faker
&lt;/h2&gt;

&lt;p&gt;This is where the second family of tools becomes attractive.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;hey-api&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Orval&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Type generation&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fetch client&lt;/td&gt;
&lt;td&gt;external &lt;code&gt;openapi-fetch&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TanStack Query&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;plugin&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zod&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;plugin&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MSW&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;limited / roadmap-dependent&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Faker&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SWR / Vue Query&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;limited&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;limited / plugin-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Orval has strong built-in support around this area.&lt;/p&gt;

&lt;p&gt;It can target tools like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TanStack Query&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SWR&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Vue Query&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Zod&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MSW&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Faker&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kubb also has a strong plugin-based approach.&lt;/p&gt;

&lt;p&gt;hey-api is more focused around SDK generation, &lt;code&gt;TanStack Query&lt;/code&gt;, and &lt;code&gt;Zod&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If your goal is mainly API clients and types, &lt;code&gt;openapi-typescript&lt;/code&gt; or hey-api may be enough.&lt;/p&gt;

&lt;p&gt;If your goal is to generate a broader frontend ecosystem from OpenAPI, Orval or Kubb becomes more interesting.&lt;/p&gt;

&lt;p&gt;In my case, I did not need &lt;code&gt;MSW&lt;/code&gt; or &lt;code&gt;Faker&lt;/code&gt; yet.&lt;/p&gt;

&lt;p&gt;So Orval and Kubb felt a bit too much for the current problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤯 Enum issue: drf-spectacular BlankEnum
&lt;/h2&gt;

&lt;p&gt;One very real-world issue was enum generation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;drf-spectacular&lt;/code&gt; can represent nullable or blank enum values like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;BlankEnum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;NullEnum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;null&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a field may use &lt;code&gt;anyOf&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;anyOf&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/StatusEnum"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/BlankEnum"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/NullEnum"&lt;/span&gt;
  &lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intended TypeScript value is something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WAITING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RUNNING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SUCCESS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FAILURE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Generated BlankEnum
&lt;/h3&gt;

&lt;p&gt;The generated output differed by tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// openapi-typescript&lt;/span&gt;
&lt;span class="nx"&gt;BlankEnum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hey-api&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;BlankEnum&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Orval&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BlankEnum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;BlankEnum&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;BlankEnum&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BlankEnum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Kubb&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blankEnumEnum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BlankEnumEnumKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;blankEnumEnum&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;blankEnumEnum&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BlankEnum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;BlankEnumEnumKey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Kubb, the empty string disappeared, and the type effectively became &lt;code&gt;never&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens after anyOf composition
&lt;/h3&gt;

&lt;p&gt;As a result, only Kubb dropped &lt;code&gt;""&lt;/code&gt; from the final type.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Can represent &lt;code&gt;""&lt;/code&gt; in the type?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hey-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not just a small type detail.&lt;/p&gt;

&lt;p&gt;It can break real application code if fields like &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;file_type&lt;/code&gt;, or &lt;code&gt;update_type&lt;/code&gt; actually receive an empty string from the backend.&lt;/p&gt;

&lt;p&gt;Possible workarounds are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;patch &lt;code&gt;BlankEnum&lt;/code&gt; after generation&lt;/li&gt;
&lt;li&gt;cast affected fields to &lt;code&gt;string | null&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;wait for an upstream fix&lt;/li&gt;
&lt;li&gt;avoid this schema shape if you control the backend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you use &lt;code&gt;drf-spectacular&lt;/code&gt; and have many &lt;code&gt;BlankEnum&lt;/code&gt; / &lt;code&gt;NullEnum&lt;/code&gt; patterns, I recommend checking this before adopting Kubb.&lt;/p&gt;




&lt;h2&gt;
  
  
  🥗 Body type reusability
&lt;/h2&gt;

&lt;p&gt;POST body types also differed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Body type style&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;components["schemas"]["ordersWriteRequest"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hey-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ordersWriteRequest&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ordersWriteRequest&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ordersCreateMutationRequest&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Kubb generated endpoint-scoped wrapper types.&lt;/p&gt;

&lt;p&gt;This can be good because each operation is very explicit.&lt;/p&gt;

&lt;p&gt;But if you want to reuse the same schema in app-level code, it can be a little less convenient.&lt;/p&gt;

&lt;p&gt;For example, if a form wants to use &lt;code&gt;ordersWriteRequest&lt;/code&gt; as its submit data type, openapi-typescript, hey-api, and Orval feel more natural.&lt;/p&gt;

&lt;p&gt;With Kubb, I may need to alias the operation-scoped type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ordersCreateMutationRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./api/kubb/types/ordersCreate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OrderWriteRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ordersCreateMutationRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not wrong.&lt;/p&gt;

&lt;p&gt;It just depends on whether your app wants shared schema types or operation-scoped types.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚪 Lint behavior
&lt;/h2&gt;

&lt;p&gt;All four tools passed &lt;code&gt;tsc --noEmit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But linting showed differences.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;oxlint default&lt;/th&gt;
&lt;th&gt;oxlint + &lt;code&gt;no-explicit-any&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;eslint recommended&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hey-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;57&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1,549&lt;/td&gt;
&lt;td&gt;1,585&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  openapi-typescript / hey-api / Orval lint errors
&lt;/h3&gt;

&lt;p&gt;For the first three tools, the eslint errors were mainly &lt;code&gt;no-irregular-whitespace&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In my schema, some Django docstrings contained full-width spaces.&lt;/p&gt;

&lt;p&gt;Those comments were copied into generated code, and eslint complained.&lt;/p&gt;

&lt;p&gt;This is more about source docstrings and lint configuration than generated code quality.&lt;/p&gt;

&lt;p&gt;It can usually be handled by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;skipping comments&lt;/li&gt;
&lt;li&gt;excluding generated files from lint&lt;/li&gt;
&lt;li&gt;adding lint overrides for generated code&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Kubb lint errors
&lt;/h3&gt;

&lt;p&gt;Kubb produced many &lt;code&gt;no-explicit-any&lt;/code&gt; errors.&lt;/p&gt;

&lt;p&gt;Most were in the runtime client side.&lt;/p&gt;

&lt;p&gt;This does not mean Kubb is unusable.&lt;/p&gt;

&lt;p&gt;But if generated files are included in lint, Kubb almost certainly needs ignore rules or overrides.&lt;/p&gt;

&lt;p&gt;Before adopting it, I would decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;will generated files be committed?&lt;/li&gt;
&lt;li&gt;will generated files be linted?&lt;/li&gt;
&lt;li&gt;will generated files be checked in CI?&lt;/li&gt;
&lt;li&gt;which rules are disabled for generated code?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📝 Configuration notes
&lt;/h2&gt;

&lt;p&gt;Here are some issues I hit while setting up each tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  openapi-typescript
&lt;/h3&gt;

&lt;p&gt;This was the easiest setup.&lt;/p&gt;

&lt;p&gt;A single CLI command worked well.&lt;/p&gt;

&lt;p&gt;I did not hit any major configuration problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  hey-api
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;hey-api&lt;/code&gt; uses &lt;code&gt;openapi-ts.config.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I hit a few small points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;output.format: "prettier"&lt;/code&gt; aborts if Prettier is not installed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;runtimeConfigPath&lt;/code&gt; is emitted as an import path relative to &lt;code&gt;output.path&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I initially assumed &lt;code&gt;runtimeConfigPath&lt;/code&gt; was relative to the project root.&lt;/p&gt;

&lt;p&gt;That was easy to misunderstand.&lt;/p&gt;

&lt;p&gt;I shared this with the author, and the related PR was merged the next day.&lt;/p&gt;

&lt;p&gt;That fast response was a positive signal for adoption.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orval
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Orval&lt;/code&gt; uses &lt;code&gt;orval.config.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It can split outputs by target, which is useful if you want to generate SDKs, Zod schemas, MSW mocks, and other assets separately.&lt;/p&gt;

&lt;p&gt;I did not hit major setup issues.&lt;/p&gt;

&lt;p&gt;But if you care about auth, refresh, or error handling, you should understand &lt;code&gt;override.mutator&lt;/code&gt; early.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kubb
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Kubb&lt;/code&gt; configuration is plugin-based.&lt;/p&gt;

&lt;p&gt;Depending on your environment, generated imports with &lt;code&gt;.ts&lt;/code&gt; extensions may require this tsconfig setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allowImportingTsExtensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not necessarily bad, but it should match your TypeScript and bundler policy.&lt;/p&gt;

&lt;p&gt;I had to adjust the setup a few times.&lt;/p&gt;




&lt;h2&gt;
  
  
  🗨️ My impression of openapi-typescript
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;openapi-typescript&lt;/code&gt; is the thinnest and fastest option.&lt;/p&gt;

&lt;p&gt;It generates types and lets you choose the runtime layer yourself, often with &lt;code&gt;openapi-fetch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What I liked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;very fast generation&lt;/li&gt;
&lt;li&gt;simple setup&lt;/li&gt;
&lt;li&gt;small generated output&lt;/li&gt;
&lt;li&gt;strong focus on type correctness&lt;/li&gt;
&lt;li&gt;runtime is separate and flexible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What may be missing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no generated SDK functions&lt;/li&gt;
&lt;li&gt;endpoint calls are path-string based&lt;/li&gt;
&lt;li&gt;weaker fit for operationId-based rename refactoring&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TanStack Query&lt;/code&gt;, &lt;code&gt;Zod&lt;/code&gt;, and &lt;code&gt;MSW&lt;/code&gt; need separate handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you value simplicity, this is very strong.&lt;/p&gt;

&lt;p&gt;Especially if codegen runs often in local development or CI, the speed is a real advantage.&lt;/p&gt;




&lt;h2&gt;
  
  
  🗨️ My impression of hey-api
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;hey-api&lt;/code&gt; felt like the best balance for my use case.&lt;/p&gt;

&lt;p&gt;It is thicker than &lt;code&gt;openapi-typescript&lt;/code&gt;, but much lighter than Orval or Kubb.&lt;/p&gt;

&lt;p&gt;What I liked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generated SDK functions&lt;/li&gt;
&lt;li&gt;consistent &lt;code&gt;{ query, path, body }&lt;/code&gt; arguments&lt;/li&gt;
&lt;li&gt;axios-like interceptors&lt;/li&gt;
&lt;li&gt;result model and &lt;code&gt;throwOnError&lt;/code&gt; option&lt;/li&gt;
&lt;li&gt;shared schema types are easy to use&lt;/li&gt;
&lt;li&gt;generated output remains manageable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Weak points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generation takes longer than &lt;code&gt;openapi-typescript&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sdk.gen.ts&lt;/code&gt; can become large&lt;/li&gt;
&lt;li&gt;weaker than Orval / Kubb for &lt;code&gt;MSW&lt;/code&gt; and &lt;code&gt;Faker&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NullEnum&lt;/code&gt; may degrade into an empty enum in some cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my case, &lt;code&gt;NullEnum&lt;/code&gt; was not a major practical issue because nullable fields still carried &lt;code&gt;nullable: true&lt;/code&gt; through the composed type.&lt;/p&gt;

&lt;p&gt;More importantly, &lt;code&gt;BlankEnum&lt;/code&gt; preserved the empty string correctly.&lt;/p&gt;

&lt;p&gt;For my needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I wanted operationId functions&lt;/li&gt;
&lt;li&gt;I wanted interceptors&lt;/li&gt;
&lt;li&gt;I wanted result-style handling&lt;/li&gt;
&lt;li&gt;I did not need generated MSW or Faker yet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So hey-api was the most practical choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  🗨️ My impression of Orval
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Orval&lt;/code&gt; is strong if you want OpenAPI to generate a broader frontend layer.&lt;/p&gt;

&lt;p&gt;What I liked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;supports &lt;code&gt;TanStack Query&lt;/code&gt;, &lt;code&gt;SWR&lt;/code&gt;, and &lt;code&gt;Vue Query&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;can generate &lt;code&gt;Zod&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;can generate &lt;code&gt;MSW&lt;/code&gt; and &lt;code&gt;Faker&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;can split outputs by target&lt;/li&gt;
&lt;li&gt;faster than Kubb in this test&lt;/li&gt;
&lt;li&gt;shared body schemas are easy to reuse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Things to watch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;default fetch client does not throw on 4xx / 5xx&lt;/li&gt;
&lt;li&gt;auth and refresh usually require a mutator&lt;/li&gt;
&lt;li&gt;file count can be large&lt;/li&gt;
&lt;li&gt;tag files can become large&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NullEnum&lt;/code&gt; may degrade into an empty const&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only want an API client, Orval may feel too thick.&lt;/p&gt;

&lt;p&gt;But if you want generated hooks, validation schemas, mocks, and test data, Orval becomes much more attractive.&lt;/p&gt;

&lt;p&gt;If I moved deeper into the ecosystem-generation side, I would probably evaluate Orval first.&lt;/p&gt;




&lt;h2&gt;
  
  
  🗨️ My impression of Kubb
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Kubb&lt;/code&gt; has a strong philosophy.&lt;/p&gt;

&lt;p&gt;What I liked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;close to 1 operation = 1 file&lt;/li&gt;
&lt;li&gt;friendly to grep and physical file navigation&lt;/li&gt;
&lt;li&gt;structure can be tree-shake friendly&lt;/li&gt;
&lt;li&gt;clear plugin-based architecture&lt;/li&gt;
&lt;li&gt;can expand into &lt;code&gt;MSW&lt;/code&gt;, &lt;code&gt;Faker&lt;/code&gt;, &lt;code&gt;TanStack Query&lt;/code&gt;, &lt;code&gt;Zod&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;gives strong control over the codegen pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What felt heavy in this schema:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;slow generation&lt;/li&gt;
&lt;li&gt;3,877 files / 24 MB output&lt;/li&gt;
&lt;li&gt;many lint errors from &lt;code&gt;any&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;possible &lt;code&gt;.ts&lt;/code&gt; import extension requirements&lt;/li&gt;
&lt;li&gt;positional path params make wrappers harder&lt;/li&gt;
&lt;li&gt;body types are endpoint-scoped&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BlankEnum&lt;/code&gt; became &lt;code&gt;never&lt;/code&gt; and lost the empty string&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Kubb&lt;/code&gt; is not a bad tool.&lt;/p&gt;

&lt;p&gt;Actually, I like its design philosophy.&lt;/p&gt;

&lt;p&gt;But if your main goal is simply "I want a type-safe API client for a normal frontend app," it may be too heavy as the first option.&lt;/p&gt;

&lt;p&gt;I would choose Kubb when I have a clear reason, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I strongly want 1 operation = 1 file&lt;/li&gt;
&lt;li&gt;I want to design my own codegen pipeline&lt;/li&gt;
&lt;li&gt;I want plugin-level control&lt;/li&gt;
&lt;li&gt;I want to unify clients, schemas, mocks, and test data through plugins&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The potential is high, but it needs commitment.&lt;/p&gt;




&lt;h2&gt;
  
  
  📓 Summary table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;hey-api&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Orval&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Family&lt;/td&gt;
&lt;td&gt;A: type-focused&lt;/td&gt;
&lt;td&gt;A: lightweight SDK&lt;/td&gt;
&lt;td&gt;B: frontend asset generation&lt;/td&gt;
&lt;td&gt;B: plugin/codegen platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generation time&lt;/td&gt;
&lt;td&gt;1.5s&lt;/td&gt;
&lt;td&gt;8.0s&lt;/td&gt;
&lt;td&gt;5.5s&lt;/td&gt;
&lt;td&gt;18.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output files&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;2,719&lt;/td&gt;
&lt;td&gt;3,877&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output size&lt;/td&gt;
&lt;td&gt;2.4 MB&lt;/td&gt;
&lt;td&gt;2.9 MB&lt;/td&gt;
&lt;td&gt;14 MB&lt;/td&gt;
&lt;td&gt;24 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generates functions&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Function split&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;one SDK file&lt;/td&gt;
&lt;td&gt;one file per tag&lt;/td&gt;
&lt;td&gt;one file per operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error model&lt;/td&gt;
&lt;td&gt;result union&lt;/td&gt;
&lt;td&gt;result union / throw&lt;/td&gt;
&lt;td&gt;success wrapper&lt;/td&gt;
&lt;td&gt;throw&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client object&lt;/td&gt;
&lt;td&gt;external library&lt;/td&gt;
&lt;td&gt;generated client&lt;/td&gt;
&lt;td&gt;raw fetch / mutator&lt;/td&gt;
&lt;td&gt;generated client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interceptor&lt;/td&gt;
&lt;td&gt;openapi-fetch middleware&lt;/td&gt;
&lt;td&gt;axios-like&lt;/td&gt;
&lt;td&gt;mutator&lt;/td&gt;
&lt;td&gt;config / custom client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path params&lt;/td&gt;
&lt;td&gt;&lt;code&gt;params.path&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ path: { id } }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ id }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;positional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Body type&lt;/td&gt;
&lt;td&gt;shared schema&lt;/td&gt;
&lt;td&gt;shared schema&lt;/td&gt;
&lt;td&gt;shared schema&lt;/td&gt;
&lt;td&gt;endpoint-scoped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TanStack Query&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;plugin&lt;/td&gt;
&lt;td&gt;built-in support&lt;/td&gt;
&lt;td&gt;plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zod&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;plugin&lt;/td&gt;
&lt;td&gt;built-in support&lt;/td&gt;
&lt;td&gt;plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MSW / Faker&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;limited&lt;/td&gt;
&lt;td&gt;strong&lt;/td&gt;
&lt;td&gt;strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lint&lt;/td&gt;
&lt;td&gt;minor adjustment&lt;/td&gt;
&lt;td&gt;minor adjustment&lt;/td&gt;
&lt;td&gt;minor adjustment&lt;/td&gt;
&lt;td&gt;likely needs ignore/override&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;drf-spectacular BlankEnum&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;needs care&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;thin and safe&lt;/td&gt;
&lt;td&gt;SDK + interceptors&lt;/td&gt;
&lt;td&gt;generate frontend assets&lt;/td&gt;
&lt;td&gt;design codegen pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🤔 How I would choose
&lt;/h2&gt;

&lt;p&gt;After this comparison, I would choose like this.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Start with openapi-typescript
&lt;/h3&gt;

&lt;p&gt;If your project already works well with &lt;code&gt;openapi-typescript + openapi-fetch&lt;/code&gt;, I would stay there.&lt;/p&gt;

&lt;p&gt;It is fast, thin, and easy to understand.&lt;/p&gt;

&lt;p&gt;A small wrapper like &lt;code&gt;src/api/client.ts&lt;/code&gt; plus middleware-based auth may be enough.&lt;/p&gt;

&lt;p&gt;There is no need to move to heavier codegen unless you have a concrete pain.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Move to hey-api when SDK functions become useful
&lt;/h3&gt;

&lt;p&gt;The next step is usually wanting things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;call APIs by operationId functions instead of URL strings&lt;/li&gt;
&lt;li&gt;grep by function name&lt;/li&gt;
&lt;li&gt;use rename refactoring&lt;/li&gt;
&lt;li&gt;handle auth and refresh with interceptors&lt;/li&gt;
&lt;li&gt;work with consistent endpoint options&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that stage, &lt;code&gt;hey-api&lt;/code&gt; is a very good fit.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Use Orval when OpenAPI becomes the source of frontend assets
&lt;/h3&gt;

&lt;p&gt;If OpenAPI becomes more than API client generation, &lt;code&gt;Orval&lt;/code&gt; becomes strong.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generated &lt;code&gt;TanStack Query&lt;/code&gt; hooks&lt;/li&gt;
&lt;li&gt;generated &lt;code&gt;Zod&lt;/code&gt; schemas&lt;/li&gt;
&lt;li&gt;generated &lt;code&gt;MSW&lt;/code&gt; mocks&lt;/li&gt;
&lt;li&gt;generated fake data&lt;/li&gt;
&lt;li&gt;multiple generation targets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where Orval's value becomes much clearer.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Choose Kubb when you love the architecture
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Kubb&lt;/code&gt; is powerful.&lt;/p&gt;

&lt;p&gt;But at this schema size, it was also heavy.&lt;/p&gt;

&lt;p&gt;I would choose it when I strongly care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 operation = 1 file&lt;/li&gt;
&lt;li&gt;plugin architecture&lt;/li&gt;
&lt;li&gt;tree-shake-friendly structure&lt;/li&gt;
&lt;li&gt;codegen pipeline control&lt;/li&gt;
&lt;li&gt;generating many kinds of artifacts consistently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It may not be my first choice for a normal frontend app, but it is very interesting if you want to design codegen itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Final conclusion
&lt;/h2&gt;

&lt;p&gt;OpenAPI codegen should not be chosen only by asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which tool has the most features?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How much generated code are we willing to own?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My final choice looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;I only want types&lt;/td&gt;
&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I want SDK functions and interceptors&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hey-api&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I want to generate frontend assets together&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I want to control codegen structure and plugins&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For my current project, &lt;code&gt;hey-api&lt;/code&gt; had the best balance of SDK functions, interceptors, result handling, and output size.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi70tk9kuqwxr09haz4hg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi70tk9kuqwxr09haz4hg.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The more useful codegen becomes, the bigger the generated forest becomes.&lt;/p&gt;

&lt;p&gt;So the important question is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who maintains that forest, how much of it, and with what team rules?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the real OpenAPI codegen decision.&lt;/p&gt;

&lt;p&gt;What does your team generate from OpenAPI?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only types?&lt;/li&gt;
&lt;li&gt;SDK functions?&lt;/li&gt;
&lt;li&gt;Hooks?&lt;/li&gt;
&lt;li&gt;Mocks?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;or the whole forest? 🌳🌳🌳&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>openapi</category>
      <category>frontend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why I Didn’t Let AI Handle My Scroll Animation: Astro, React, and TypeScript Architecture</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Thu, 07 May 2026 12:41:31 +0000</pubDate>
      <link>https://dev.to/nyaomaru/why-i-didnt-let-ai-handle-my-scroll-animation-astro-react-and-typescript-architecture-37k2</link>
      <guid>https://dev.to/nyaomaru/why-i-didnt-let-ai-handle-my-scroll-animation-astro-react-and-typescript-architecture-37k2</guid>
      <description>&lt;p&gt;Hoi hoi!&lt;/p&gt;

&lt;p&gt;I'm &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;@nyaomaru&lt;/a&gt;, a frontend engineer who once panicked because I triggered a fire alarm while roasting chashu at home in the Netherlands. 🐖&lt;/p&gt;

&lt;p&gt;This time, I want to write about how I built the corporate website for &lt;strong&gt;Necoz B.V.&lt;/strong&gt;&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://necoz.co/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnecoz.co%2Fnecoz_ogp.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://necoz.co/" rel="noopener noreferrer" class="c-link"&gt;
            Necoz | Software Development Studio in Amsterdam
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Necoz B.V. is a software development studio based in Amsterdam, the Netherlands. We build well-designed systems, clean architecture, and scalable web applications.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnecoz.co%2Ffavicon.png"&gt;
          necoz.co
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The site is built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Astro&lt;/code&gt; for the base structure&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;React&lt;/code&gt; only where interactive UI is needed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TypeScript&lt;/code&gt; for the main animation control&lt;/li&gt;
&lt;li&gt;responsive behavior for both desktop and mobile&lt;/li&gt;
&lt;li&gt;virtual scrolling to control the scroll amount itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because this is a corporate website, I did not want to break SEO.&lt;/p&gt;

&lt;p&gt;But I also wanted the motion to feel good.&lt;/p&gt;

&lt;p&gt;And I did not want the experience to fall apart on mobile.&lt;/p&gt;

&lt;p&gt;When you take these requirements seriously, the important question is not only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which technology should I use?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It becomes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which responsibility should belong to which layer?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the end, the hardest part was the animation.&lt;/p&gt;

&lt;p&gt;AI was useful. It helped me move fast. It gave me rough ideas and initial implementations.&lt;/p&gt;

&lt;p&gt;But the animation it produced was not something I could use as-is.&lt;/p&gt;

&lt;p&gt;This article is about why, and how I ended up designing the animation system.&lt;/p&gt;

&lt;p&gt;The repository is here:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;
        nyaomaru
      &lt;/a&gt; / &lt;a href="https://github.com/nyaomaru/necoz" rel="noopener noreferrer"&gt;
        necoz
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Official Necoz B.V. website with custom scroll animations.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;necoz&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/nyaomaru/necoz/main/public/necoz_ogp.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fnecoz%2Fmain%2Fpublic%2Fnecoz_ogp.png" alt="necoz logo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Necoz B.V. website built with Astro, React islands, and custom scroll / walker animation logic.&lt;/p&gt;
&lt;p&gt;Scroll down and enjoy the animations!&lt;/p&gt;
&lt;p&gt;You can check it on mobile too 😸&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Project Structure&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;The project is organized like this:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;/
├── public/                     # Static files served as-is
├── src/
│   ├── components/
│   │   ├── home/               # Homepage sections
│   │   └── ui/                 # Reusable UI primitives and shared shells
│   │       ├── block/
│   │       ├── button/
│   │       ├── layout/
│   │       ├── mail/
│   │       ├── nyaomaru/
│   │       ├── scene/
│   │       ├── scroll/
│   │       └── text/
│   ├── layouts/                # Shared page layout shells
│   ├── lib/                    # Non-visual shared logic
│   │   ├── nyaomaru/           # Walker controller, scene logic, scene models
│   │   ├── math.ts
│   │   └── site-links.ts
│   ├── pages/                  # Route entrypoints
│   │   ├── index.astro
│   │   └── privacy-policy.astro
│&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nyaomaru/necoz" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Let's get into it.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 Why I did not use AI-generated animation as-is
&lt;/h2&gt;

&lt;p&gt;AI-generated animation often looks nice at first glance.&lt;/p&gt;

&lt;p&gt;It usually has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;some movement&lt;/li&gt;
&lt;li&gt;some easing&lt;/li&gt;
&lt;li&gt;some visual atmosphere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But when I looked more carefully, I often felt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The direction is right, but the timing is wrong.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the movement was too fast&lt;/li&gt;
&lt;li&gt;everything moved with the same rhythm&lt;/li&gt;
&lt;li&gt;elements did not stop where they should&lt;/li&gt;
&lt;li&gt;there was no pause&lt;/li&gt;
&lt;li&gt;there was no sense of timing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI can generate the rough idea of "how something moves".&lt;/p&gt;

&lt;p&gt;But in real animation, what matters is also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;where it stops&lt;/li&gt;
&lt;li&gt;where it waits a little&lt;/li&gt;
&lt;li&gt;where one element reacts slightly later&lt;/li&gt;
&lt;li&gt;where the rhythm changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Animation is not just about moving from position A to position B.&lt;/p&gt;

&lt;p&gt;A small pause, a tiny delay, or a slightly different easing curve can completely change how it feels.&lt;/p&gt;

&lt;p&gt;I think this is one of the current gaps between AI and human judgment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AI can produce motion quickly, but humans still have better sensitivity for timing and rhythm.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I used AI for rough drafts, structure, and experiments.&lt;/p&gt;

&lt;p&gt;But I adjusted the final timing by hand.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧑‍🚀 Why Astro?
&lt;/h2&gt;

&lt;p&gt;This site is a corporate website.&lt;/p&gt;

&lt;p&gt;So I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;real HTML content&lt;/li&gt;
&lt;li&gt;good SEO&lt;/li&gt;
&lt;li&gt;fast initial loading&lt;/li&gt;
&lt;li&gt;no unnecessary SPA architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why I chose &lt;code&gt;Astro&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://astro.build/" rel="noopener noreferrer"&gt;https://astro.build/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The page structure is mainly built around &lt;code&gt;src/pages/index.astro&lt;/code&gt; and &lt;code&gt;src/layouts/Layout.astro&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The header, sections, footer, and base layout are handled by Astro.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Layout&lt;/span&gt; &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;{DEFAULT_TITLE}&lt;/span&gt; &lt;span class="na"&gt;description=&lt;/span&gt;&lt;span class="s"&gt;{DEFAULT_DESCRIPTION}&lt;/span&gt; &lt;span class="na"&gt;canonicalPath=&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;main&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"top"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Header&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;HeroIntro&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;WorkSection&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;StudioSection&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ContactSection&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;NyaomaruWalker&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Footer&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Layout&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The layout also switches between native scrolling and virtual scrolling.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;body&lt;/span&gt; &lt;span class="na"&gt;class:list=&lt;/span&gt;&lt;span class="s"&gt;{[virtualScroll&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;virtual-scroll-body&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="na"&gt;:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;native-scroll-body&lt;/span&gt;&lt;span class="err"&gt;"]}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  {virtualScroll ? &lt;span class="nt"&gt;&amp;lt;VirtualScroll&amp;gt;&amp;lt;slot&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&amp;lt;/VirtualScroll&amp;gt;&lt;/span&gt; : &lt;span class="nt"&gt;&amp;lt;slot&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;}
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;What I like about &lt;code&gt;Astro&lt;/code&gt; is that I do not have to turn the whole site into JavaScript.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Astro&lt;/code&gt; lets me output clean HTML first, and then hydrate only the parts that need client-side behavior.&lt;/p&gt;

&lt;p&gt;For a corporate website, this is very useful.&lt;/p&gt;

&lt;p&gt;In this project, I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;solid page structure&lt;/li&gt;
&lt;li&gt;metadata and SEO&lt;/li&gt;
&lt;li&gt;client-side animation only where necessary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Astro&lt;/code&gt; did not make the animation better by itself.&lt;/p&gt;

&lt;p&gt;But it helped me separate structure and behavior cleanly.&lt;/p&gt;

&lt;p&gt;That separation was very important.&lt;/p&gt;


&lt;h2&gt;
  
  
  ⚖️ I used React, but not for everything
&lt;/h2&gt;

&lt;p&gt;The site also uses &lt;code&gt;React&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But it is not a full React app.&lt;/p&gt;

&lt;p&gt;The responsibility is divided like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Astro&lt;/code&gt; owns the page structure&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;React&lt;/code&gt; owns small interactive UI parts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TypeScript&lt;/code&gt; owns the animation system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, the mail dialog is hydrated with &lt;code&gt;client:load&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"contact-links"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;MailDialogTrigger&lt;/span&gt; &lt;span class="na"&gt;client:load&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;SocialLinks&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The dialog trigger itself is a React component.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;MailDialogTrigger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsOpen&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;previousOverflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;previousOverflow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is a good use case for &lt;code&gt;React&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is local UI state.&lt;/p&gt;

&lt;p&gt;There is user interaction.&lt;/p&gt;

&lt;p&gt;There is a dialog.&lt;/p&gt;

&lt;p&gt;But that does not mean the entire website needs to become a React application.&lt;/p&gt;

&lt;p&gt;This was an important decision:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Using React does not mean everything has to be React.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;React is great for UI.&lt;/p&gt;

&lt;p&gt;But I did not want React to become responsible for the motion system.&lt;/p&gt;


&lt;h2&gt;
  
  
  🌊 Animation is controlled by TypeScript, not React state
&lt;/h2&gt;

&lt;p&gt;The core animation logic lives under &lt;code&gt;src/lib/nyaomaru/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It handles things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the walking character&lt;/li&gt;
&lt;li&gt;scene progression&lt;/li&gt;
&lt;li&gt;scroll-based state updates&lt;/li&gt;
&lt;li&gt;DOM measurement&lt;/li&gt;
&lt;li&gt;animation phases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of the main files are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;walker-controller.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Controls the main walker progression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;work-scene.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Controls the work section scene&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;studio-scene.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Controls the studio section scene&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;contact-scene.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Controls the contact section scene&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not a purely declarative animation system.&lt;/p&gt;

&lt;p&gt;It is much more direct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read the current scroll position&lt;/li&gt;
&lt;li&gt;determine the active scene&lt;/li&gt;
&lt;li&gt;calculate progress&lt;/li&gt;
&lt;li&gt;calculate the pose&lt;/li&gt;
&lt;li&gt;update the DOM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simplified version looks like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scrollY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSceneScrollY&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;activeSceneState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getActiveSceneState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scrollY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;activeSceneState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSceneProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scrollY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;activeSceneState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;activeSceneState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;activeSceneState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;walker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`translate(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;walker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;cancelAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rafId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;rafId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The flow is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;read the scroll value&lt;/li&gt;
&lt;li&gt;find the active scene&lt;/li&gt;
&lt;li&gt;calculate progress&lt;/li&gt;
&lt;li&gt;calculate the pose&lt;/li&gt;
&lt;li&gt;apply it to the DOM&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This was easier to tune than putting everything into React state.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;React&lt;/code&gt; is strong for UI state.&lt;/p&gt;

&lt;p&gt;But animation timing often needs a thinner, more direct control layer.&lt;/p&gt;

&lt;p&gt;This does not mean React is bad for animation.&lt;/p&gt;

&lt;p&gt;It only means that, for this project, the responsibilities were different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React was for UI&lt;/li&gt;
&lt;li&gt;TypeScript was for motion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation worked well.&lt;/p&gt;


&lt;h2&gt;
  
  
  💎 Why I did not avoid direct DOM manipulation
&lt;/h2&gt;

&lt;p&gt;In modern frontend development, we often try to avoid direct DOM manipulation.&lt;/p&gt;

&lt;p&gt;Usually, I agree with that direction.&lt;/p&gt;

&lt;p&gt;Declarative UI is easier to reason about in many cases.&lt;/p&gt;

&lt;p&gt;But scroll-driven animation is a little different.&lt;/p&gt;

&lt;p&gt;Sometimes, the important state is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What state does this component have?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What is actually visible on the screen right now?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For this project, I needed to measure things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getBoundingClientRect()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;viewport width&lt;/li&gt;
&lt;li&gt;which block is currently visible&lt;/li&gt;
&lt;li&gt;whether the desktop or mobile layout is active&lt;/li&gt;
&lt;li&gt;where the walker is currently positioned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was similar to what I learned while building a browser game before:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/nyaomaru/building-a-browser-game-with-react-what-doesnt-work-well-run-away-from-work-1khf" class="crayons-story__hidden-navigation-link"&gt;Building a Browser Game with React: What Doesn’t work well (Run Away From Work)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/nyaomaru" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1436158%2Fa78bc4ed-7796-4760-baa5-06283c2404d6.png" alt="nyaomaru profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/nyaomaru" class="crayons-story__secondary fw-medium m:hidden"&gt;
              nyaomaru
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                nyaomaru
                
              
              &lt;div id="story-author-preview-content-3466423" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/nyaomaru" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1436158%2Fa78bc4ed-7796-4760-baa5-06283c2404d6.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;nyaomaru&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/nyaomaru/building-a-browser-game-with-react-what-doesnt-work-well-run-away-from-work-1khf" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 8&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/nyaomaru/building-a-browser-game-with-react-what-doesnt-work-well-run-away-from-work-1khf" id="article-link-3466423"&gt;
          Building a Browser Game with React: What Doesn’t work well (Run Away From Work)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gamedev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gamedev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/typescript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;typescript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/react"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;react&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/nyaomaru/building-a-browser-game-with-react-what-doesnt-work-well-run-away-from-work-1khf" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;41&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/nyaomaru/building-a-browser-game-with-react-what-doesnt-work-well-run-away-from-work-1khf#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              3&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;The animation depends on the actual layout.&lt;/p&gt;

&lt;p&gt;So the DOM is not just an output target.&lt;/p&gt;

&lt;p&gt;It is also something the animation system has to observe.&lt;/p&gt;

&lt;p&gt;In this case, directly measuring the DOM made the intention clearer.&lt;/p&gt;

&lt;p&gt;It also avoided unnecessary indirection.&lt;/p&gt;

&lt;p&gt;So I did not avoid direct DOM manipulation completely.&lt;/p&gt;

&lt;p&gt;I used it only where it made sense.&lt;/p&gt;




&lt;h2&gt;
  
  
  😿 Responsive design was not only a CSS problem
&lt;/h2&gt;

&lt;p&gt;The site also supports mobile.&lt;/p&gt;

&lt;p&gt;But responsive behavior here was not only about changing layout with CSS.&lt;/p&gt;

&lt;p&gt;For several sections, I have different block structures for desktop and mobile.&lt;/p&gt;

&lt;p&gt;For example, the hero, work, and studio sections have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;desktop blocks&lt;/li&gt;
&lt;li&gt;mobile blocks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The visible block changes depending on the viewport.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;TypeScript&lt;/code&gt; animation logic also uses a &lt;code&gt;430px&lt;/code&gt; mobile breakpoint and changes things like scene progress, landing positions, and offsets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workStack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isMobileViewport&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;getVisibleElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[data-work-mobile-origin]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getVisibleElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[data-work-scene-display]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stackRect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;workStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;walkerRect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;walker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shotX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;walkerRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isMobileViewport&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;MOBILE_SHOT_OFFSET_X&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SHOT_OFFSET_X&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The markup also separates desktop and mobile blocks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"block block-three block-three--desktop"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"block block-three block-three--mobile"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So responsive design in this project affected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DOM structure&lt;/li&gt;
&lt;li&gt;scroll progression&lt;/li&gt;
&lt;li&gt;animation targets&lt;/li&gt;
&lt;li&gt;movement distance&lt;/li&gt;
&lt;li&gt;position correction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I handled only the visual layout, the desktop version might feel good, but the mobile version would break quickly.&lt;/p&gt;

&lt;p&gt;For scroll-driven animation, responsive design is not only layout adjustment.&lt;/p&gt;

&lt;p&gt;It is also motion redesign.&lt;/p&gt;




&lt;h2&gt;
  
  
  📏 Why I used virtual scroll
&lt;/h2&gt;

&lt;p&gt;One of the most important parts of this site is virtual scrolling.&lt;/p&gt;

&lt;p&gt;The layout uses &lt;code&gt;VirtualScroll.astro&lt;/code&gt;, and inside that layer, a custom scroll controller runs.&lt;/p&gt;

&lt;p&gt;The goal was not just to make scrolling look fancy.&lt;/p&gt;

&lt;p&gt;The real goal was to separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;visual scroll&lt;/li&gt;
&lt;li&gt;scene scroll&lt;/li&gt;
&lt;li&gt;scrollbar behavior&lt;/li&gt;
&lt;li&gt;smooth scroll following&lt;/li&gt;
&lt;li&gt;animation progression&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4s4wn0ozo2q1thg9pfbw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4s4wn0ozo2q1thg9pfbw.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the implementation, I separate &lt;code&gt;visualScrollY&lt;/code&gt; and &lt;code&gt;sceneScrollY&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;applyScroll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visualScrollY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getVisualScrollY&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sceneScrollY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSceneScrollY&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;visualScrollY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;setScrollState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sceneScrollY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;visualScrollY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;necoz:virtual-scroll&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scroll values are read like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getVisualScrollY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__necozScrollY&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getSceneScrollY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;visualScrollY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getVisualScrollY&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__necozSceneScrollY&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;visualScrollY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation was very important.&lt;/p&gt;

&lt;p&gt;If everything depends directly on native scroll, it becomes harder to control the rhythm.&lt;/p&gt;

&lt;p&gt;For example, I wanted to adjust cases like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the page visually moved down, but the scene should wait a little more&lt;/li&gt;
&lt;li&gt;the footer area needs denser animation&lt;/li&gt;
&lt;li&gt;mobile should use a different scroll progression&lt;/li&gt;
&lt;li&gt;some scenes should feel slower or more deliberate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With virtual scroll, I can separate:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;how much the page visually moves&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;how much the animation scene progresses&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That means I can design the time axis myself.&lt;/p&gt;

&lt;p&gt;This was the main reason I used virtual scroll.&lt;/p&gt;

&lt;p&gt;Not because it looks cool.&lt;/p&gt;

&lt;p&gt;But because I wanted control over animation time.&lt;/p&gt;

&lt;p&gt;For scroll-driven animation, scroll is not just movement.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Scroll is input.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And if scroll is input, it needs to be designed carefully.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤖 Where AI was useful
&lt;/h2&gt;

&lt;p&gt;At this point, it might sound like I do not trust AI.&lt;/p&gt;

&lt;p&gt;That is not true.&lt;/p&gt;

&lt;p&gt;AI was very useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generating rough drafts&lt;/li&gt;
&lt;li&gt;trying implementation patterns&lt;/li&gt;
&lt;li&gt;splitting responsibilities&lt;/li&gt;
&lt;li&gt;getting something working quickly&lt;/li&gt;
&lt;li&gt;exploring ideas before committing to one direction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a tool for the first step, AI is excellent.&lt;/p&gt;

&lt;p&gt;But final animation tuning was different.&lt;/p&gt;

&lt;p&gt;For example, walking, falling, and landing phases were separated like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lerp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;secondFallStart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secondFallX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;landingX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secondFallProgress&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secondRunY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;landingY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secondFallProgress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HERO_FALL_EASING_POWER&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;second-fall&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;secondRunStart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstFallX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secondFallX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secondRunProgress&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secondRunY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;second-run&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of adjustment required a repeated loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;look at the animation&lt;/li&gt;
&lt;li&gt;feel that something is slightly wrong&lt;/li&gt;
&lt;li&gt;adjust a function or a number&lt;/li&gt;
&lt;li&gt;check it again&lt;/li&gt;
&lt;li&gt;repeat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI helped me move fast.&lt;/p&gt;

&lt;p&gt;But speed alone was not enough.&lt;/p&gt;

&lt;p&gt;For this kind of site, the character walks, the scene changes, and the rhythm follows the scroll.&lt;/p&gt;

&lt;p&gt;The final feeling had to be adjusted by hand.&lt;/p&gt;

&lt;p&gt;I think this is one of the areas where frontend engineers will still matter in the AI era.&lt;/p&gt;

&lt;p&gt;Not only writing code faster.&lt;/p&gt;

&lt;p&gt;But deciding whether the result actually feels good.&lt;/p&gt;




&lt;h2&gt;
  
  
  💖 What I learned from this architecture
&lt;/h2&gt;

&lt;p&gt;The main lessons from this project are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;separate HTML structure and animation behavior&lt;/li&gt;
&lt;li&gt;separate UI state and animation state&lt;/li&gt;
&lt;li&gt;treat scroll as input, not just movement&lt;/li&gt;
&lt;li&gt;responsive animation requires motion redesign, not only layout changes&lt;/li&gt;
&lt;li&gt;direct DOM measurement can be valid when the screen itself is part of the state&lt;/li&gt;
&lt;li&gt;AI is useful for speed, but timing and rhythm still need human judgment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3zdm6v4aoj0j3t3uktwm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3zdm6v4aoj0j3t3uktwm.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For scroll-driven animation, this kind of separation made the system much easier to reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Summary
&lt;/h2&gt;

&lt;p&gt;For the Necoz B.V. website, I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Astro&lt;/code&gt; for page structure and initial HTML&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;React&lt;/code&gt; for small interactive UI parts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TypeScript&lt;/code&gt; for the main animation control&lt;/li&gt;
&lt;li&gt;direct DOM measurement where needed&lt;/li&gt;
&lt;li&gt;responsive motion logic for desktop and mobile&lt;/li&gt;
&lt;li&gt;virtual scroll to redesign scroll progression&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest benefit was that I could keep SEO and initial rendering stable while still keeping control over the motion system.&lt;/p&gt;

&lt;p&gt;AI helped a lot.&lt;/p&gt;

&lt;p&gt;But there is still a gap between:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;animation that moves&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;animation that feels alive&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I did not let AI handle everything.&lt;/p&gt;

&lt;p&gt;I used it to move faster, organize ideas, and create rough versions.&lt;/p&gt;

&lt;p&gt;But I kept the final timing and rhythm in my own hands.&lt;/p&gt;

&lt;p&gt;In the end, I think what I was really building was not just a layout or an animation.&lt;/p&gt;

&lt;p&gt;I was designing time along the scroll.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

&lt;p&gt;If you found this interesting, the repository is public.&lt;/p&gt;

&lt;p&gt;A star ⭐ would make me very happy 😸&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/necoz" rel="noopener noreferrer"&gt;https://github.com/nyaomaru/necoz&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>astro</category>
      <category>react</category>
    </item>
    <item>
      <title>Handling `unknown` in TypeScript… isn't it painful?</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Tue, 28 Apr 2026 13:09:54 +0000</pubDate>
      <link>https://dev.to/nyaomaru/handling-unknown-in-typescript-isnt-it-painful-4dec</link>
      <guid>https://dev.to/nyaomaru/handling-unknown-in-typescript-isnt-it-painful-4dec</guid>
      <description>&lt;p&gt;Hi there 👋&lt;/p&gt;

&lt;p&gt;I'm a frontend engineer based in the Netherlands, currently suffering from hay fever 😿&lt;/p&gt;

&lt;p&gt;API responses, form inputs, external data…&lt;/p&gt;

&lt;p&gt;In TypeScript, we often end up dealing with &lt;strong&gt;&lt;code&gt;unknown&lt;/code&gt;&lt;/strong&gt;,&lt;br&gt;
and handling it properly can become a real pain in day-to-day work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1n1jxhr8qai9f63jr5mi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1n1jxhr8qai9f63jr5mi.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yes, &lt;code&gt;unknown&lt;/code&gt; has a kind of &lt;strong&gt;gravitational pull like the universe&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But still…&lt;br&gt;
we &lt;em&gt;do&lt;/em&gt; want to handle types safely, right?&lt;/p&gt;

&lt;p&gt;That’s where &lt;strong&gt;&lt;code&gt;is-kit&lt;/code&gt;&lt;/strong&gt; comes in a library for composing type guards.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fah7dsw9wjzpexas5fbuz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fah7dsw9wjzpexas5fbuz.png" alt=" " width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;
        nyaomaru
      &lt;/a&gt; / &lt;a href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;
        is-kit
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Lightweight, zero-dependency toolkit for building `isFoo` style type guards in TypeScript. Runtime-safe 🛡️, composable 🧩, and ergonomic ✨. npm -&amp;gt; https://www.npmjs.com/package/is-kit
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;is-kit&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/nyaomaru/is-kit/main/docs/public/iskit_image.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fis-kit%2Fmain%2Fdocs%2Fpublic%2Fiskit_image.png" width="600" alt="is-kit logo"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://www.npmjs.com/package/is-kit" rel="nofollow noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/cb829c46f855a6f184f255391ea5fe58bb6ed17112e34d4856acd61dc63179c2/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f69732d6b69742e737667" alt="npm version"&gt;
  &lt;/a&gt;
  &lt;a href="https://jsr.io/@nyaomaru/is-kit" rel="nofollow noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/b6648f6bf32eb9dff2136ce8538ccb4cc6af537c1127292b5fb0de416f7a69b4/68747470733a2f2f696d672e736869656c64732e696f2f6a73722f762f406e79616f6d6172752f69732d6b6974" alt="JSR"&gt;
  &lt;/a&gt;
  &lt;a href="https://www.npmjs.com/package/is-kit" rel="nofollow noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/9b30eeaac46ebced99b5e0dc290fd045bc34c738c66b5d95c4b6ee8fed358b34/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f64742f69732d6b69742e737667" alt="npm downloads"&gt;
  &lt;/a&gt;
  &lt;a href="https://github.com/nyaomaru/is-kit/blob/main/LICENSE" rel="noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/631cdf8c56ad84dd8657fe3371b93015b5474c517422f968ff4ec944bb28bf2f/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f6c2f69732d6b69742e7376673f73616e6974697a653d74727565" alt="License"&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; is a lightweight, zero-dependency toolkit for building reusable TypeScript &lt;strong&gt;type guards&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It helps you write small &lt;code&gt;isFoo&lt;/code&gt; functions, compose them into &lt;strong&gt;richer runtime checks&lt;/strong&gt;, and keep &lt;strong&gt;TypeScript narrowing&lt;/strong&gt; natural inside regular control flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime-safe&lt;/strong&gt; 🛡️, &lt;strong&gt;composable&lt;/strong&gt; 🧩, and &lt;strong&gt;ergonomic&lt;/strong&gt; ✨ without asking you to adopt a heavy schema workflow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build and reuse &lt;strong&gt;typed guards&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose guards&lt;/strong&gt; with &lt;code&gt;and&lt;/code&gt;, &lt;code&gt;or&lt;/code&gt;, &lt;code&gt;not&lt;/code&gt;, &lt;code&gt;oneOf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate object&lt;/strong&gt; shapes and collections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parse or assert&lt;/strong&gt; &lt;code&gt;unknown&lt;/code&gt; values without a large schema framework&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://is-kit-docs.vercel.app/" rel="nofollow noopener noreferrer"&gt;📚 Documentation Site&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Best for &lt;strong&gt;app-internal narrowing, filtering, and reusable guards&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🤔 Why use &lt;code&gt;is-kit&lt;/code&gt;?&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Tired of rewriting the same &lt;code&gt;isFoo&lt;/code&gt; checks again and again?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; is a good fit when you want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;write reusable &lt;code&gt;isX&lt;/code&gt;&lt;/strong&gt; functions instead of one-off inline checks&lt;/li&gt;
&lt;li&gt;keep runtime validation &lt;strong&gt;lightweight and dependency-free&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;narrow values directly&lt;/strong&gt; in &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, and other TypeScript control flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;compose validation logic&lt;/strong&gt;…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Libraries like &lt;code&gt;Zod&lt;/code&gt; take a &lt;strong&gt;schema-first approach&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In contrast, &lt;strong&gt;&lt;code&gt;is-kit&lt;/code&gt; focuses on narrowing types &lt;em&gt;within your existing code&lt;/em&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of “writing validation”, you can think of it as &lt;strong&gt;extending your everyday &lt;code&gt;if&lt;/code&gt; statements with type safety&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;Zod&lt;/code&gt; is for &lt;strong&gt;boundaries (API / input)&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;is-kit&lt;/code&gt; is for &lt;strong&gt;inside your application logic&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  A simple example
&lt;/h2&gt;

&lt;p&gt;Imagine you need to check “strings with length ≤ 3” multiple times.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;is-kit&lt;/code&gt;, you can define it once and reuse it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;define&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is-kit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isShortString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;define&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then use it like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isShortString&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~/utils/is&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// before → repeating conditions every time&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// after → reusable guard&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isShortString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbsw247artoxxa6dbvb9b.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbsw247artoxxa6dbvb9b.gif" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This style works seamlessly with&lt;br&gt;
&lt;code&gt;if&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt;, and other existing logic.&lt;/p&gt;


&lt;h2&gt;
  
  
  🐾 How &lt;code&gt;is-kit&lt;/code&gt; has evolved
&lt;/h2&gt;

&lt;p&gt;It’s been about 6 months since the &lt;code&gt;v1.0&lt;/code&gt; release.&lt;/p&gt;

&lt;p&gt;Back then, &lt;code&gt;is-kit&lt;/code&gt; started as a &lt;strong&gt;lightweight type guard library&lt;/strong&gt;&lt;br&gt;
with primitives like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;define&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;and&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;or&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;struct&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arrayOf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since then, up to &lt;code&gt;v1.6&lt;/code&gt;, it has gradually evolved into something more practical:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;a toolkit for handling &lt;code&gt;unknown&lt;/code&gt; values in real-world applications&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s look at 5 major improvements.&lt;/p&gt;




&lt;h2&gt;
  
  
  🪄 1. Distinguishing “missing” vs “undefined” in &lt;code&gt;struct&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A common situation in API responses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A key is missing entirely&lt;/li&gt;
&lt;li&gt;A key exists but its value is &lt;code&gt;undefined&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are &lt;em&gt;not the same&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;v1.5.0&lt;/code&gt;, &lt;code&gt;optionalKey(...)&lt;/code&gt; was introduced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;optionalKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is-kit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;nickname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;optionalKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;optionalKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This allows you to express both cases explicitly.&lt;/p&gt;


&lt;h2&gt;
  
  
  🔑 2. Key-based narrowing (&lt;code&gt;hasKey&lt;/code&gt;, &lt;code&gt;hasKeys&lt;/code&gt;, &lt;code&gt;narrowKeyTo&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;From &lt;code&gt;v1.1.13&lt;/code&gt; and &lt;code&gt;v1.4.0&lt;/code&gt;, key-based utilities were added:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;hasKeys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;narrowKeyTo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;oneOfValues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is-kit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;oneOfValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;guest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trial&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasRoleAndId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hasKeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;byRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;narrowKeyTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isGuest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;byRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;guest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This lets you &lt;strong&gt;build on top of existing guards&lt;/strong&gt; instead of redefining everything.&lt;/p&gt;


&lt;h2&gt;
  
  
  🧪 3. &lt;code&gt;assert&lt;/code&gt; for fail-fast validation
&lt;/h2&gt;

&lt;p&gt;Added in &lt;code&gt;v1.2.0&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is-kit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input must be a string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You can now use guards in a &lt;strong&gt;fail-fast style&lt;/strong&gt; when needed.&lt;/p&gt;


&lt;h2&gt;
  
  
  ✨ 4. Support for &lt;code&gt;Set&lt;/code&gt; and &lt;code&gt;Map&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;v1.6.0&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mapOf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setOf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isNumber&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is-kit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isTags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isScores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isNumber&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Not everything is an array in real-world apps —&lt;br&gt;
this expands coverage to common JS data structures.&lt;/p&gt;


&lt;h2&gt;
  
  
  🥏 5. Handling numeric edge cases
&lt;/h2&gt;

&lt;p&gt;From &lt;code&gt;v1.1.x&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;isInteger&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isSafeInteger&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isPositive&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isNegative&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isNaN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isInfiniteNumber&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isZero&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These help you express &lt;strong&gt;“valid numbers” more precisely&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  🌟 More features
&lt;/h2&gt;

&lt;p&gt;There are more utilities available —&lt;br&gt;
feel free to explore the docs:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://is-kit-docs.vercel.app/en" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;is-kit-docs.vercel.app&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;






&lt;h2&gt;
  
  
  🎯 Summary
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; started as a small, composable type guard library.&lt;/p&gt;

&lt;p&gt;Over time, it has evolved into:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;a practical toolkit for handling &lt;code&gt;unknown&lt;/code&gt; in application code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Key improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More expressive object schemas (&lt;code&gt;optionalKey&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Better key-based narrowing&lt;/li&gt;
&lt;li&gt;Fail-fast assertions&lt;/li&gt;
&lt;li&gt;Collection support (&lt;code&gt;Set&lt;/code&gt;, &lt;code&gt;Map&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Rich numeric guards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal of &lt;code&gt;is-kit&lt;/code&gt; is simple:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Make type-safe code feel natural to write&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you try it and have ideas or feedback, feel free to share!&lt;/p&gt;

&lt;p&gt;See you in the next post 👋&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;https://github.com/nyaomaru/is-kit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>opensource</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>is-kit vs Zod: A Practical Comparison from 3 Perspectives</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Wed, 22 Apr 2026 12:32:02 +0000</pubDate>
      <link>https://dev.to/nyaomaru/is-kit-vs-zod-a-practical-comparison-from-3-perspectives-21eh</link>
      <guid>https://dev.to/nyaomaru/is-kit-vs-zod-a-practical-comparison-from-3-perspectives-21eh</guid>
      <description>&lt;p&gt;Hi everyone!&lt;/p&gt;

&lt;p&gt;I’m a frontend engineer who sometimes wonders:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Am I using AI… or is AI using me?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhsmsau4fc655s5n4hrpj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhsmsau4fc655s5n4hrpj.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When writing runtime validation in TypeScript,&lt;br&gt;&lt;br&gt;
&lt;a href="https://zod.dev" rel="noopener noreferrer"&gt;&lt;code&gt;Zod&lt;/code&gt;&lt;/a&gt; is usually the first choice.&lt;/p&gt;

&lt;p&gt;There are other options like &lt;code&gt;Valibot&lt;/code&gt; and &lt;code&gt;ArkType&lt;/code&gt;,&lt;/p&gt;

&lt;p&gt;but today I want to look at something a bit different:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;
        nyaomaru
      &lt;/a&gt; / &lt;a href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;
        is-kit
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Lightweight, zero-dependency toolkit for building `isFoo` style type guards in TypeScript. Runtime-safe 🛡️, composable 🧩, and ergonomic ✨. npm -&amp;gt; https://www.npmjs.com/package/is-kit
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;is-kit&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/nyaomaru/is-kit/main/docs/public/iskit_image.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fis-kit%2Fmain%2Fdocs%2Fpublic%2Fiskit_image.png" width="600" alt="is-kit logo"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://www.npmjs.com/package/is-kit" rel="nofollow noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/cb829c46f855a6f184f255391ea5fe58bb6ed17112e34d4856acd61dc63179c2/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f69732d6b69742e737667" alt="npm version"&gt;
  &lt;/a&gt;
  &lt;a href="https://jsr.io/@nyaomaru/is-kit" rel="nofollow noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/b6648f6bf32eb9dff2136ce8538ccb4cc6af537c1127292b5fb0de416f7a69b4/68747470733a2f2f696d672e736869656c64732e696f2f6a73722f762f406e79616f6d6172752f69732d6b6974" alt="JSR"&gt;
  &lt;/a&gt;
  &lt;a href="https://www.npmjs.com/package/is-kit" rel="nofollow noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/9b30eeaac46ebced99b5e0dc290fd045bc34c738c66b5d95c4b6ee8fed358b34/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f64742f69732d6b69742e737667" alt="npm downloads"&gt;
  &lt;/a&gt;
  &lt;a href="https://github.com/nyaomaru/is-kit/blob/main/LICENSE" rel="noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/631cdf8c56ad84dd8657fe3371b93015b5474c517422f968ff4ec944bb28bf2f/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f6c2f69732d6b69742e7376673f73616e6974697a653d74727565" alt="License"&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; is a lightweight, zero-dependency toolkit for building reusable TypeScript &lt;strong&gt;type guards&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It helps you write small &lt;code&gt;isFoo&lt;/code&gt; functions, compose them into &lt;strong&gt;richer runtime checks&lt;/strong&gt;, and keep &lt;strong&gt;TypeScript narrowing&lt;/strong&gt; natural inside regular control flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime-safe&lt;/strong&gt; 🛡️, &lt;strong&gt;composable&lt;/strong&gt; 🧩, and &lt;strong&gt;ergonomic&lt;/strong&gt; ✨ without asking you to adopt a heavy schema workflow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build and reuse &lt;strong&gt;typed guards&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose guards&lt;/strong&gt; with &lt;code&gt;and&lt;/code&gt;, &lt;code&gt;or&lt;/code&gt;, &lt;code&gt;not&lt;/code&gt;, &lt;code&gt;oneOf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate object&lt;/strong&gt; shapes and collections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parse or assert&lt;/strong&gt; &lt;code&gt;unknown&lt;/code&gt; values without a large schema framework&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://is-kit-docs.vercel.app/" rel="nofollow noopener noreferrer"&gt;📚 Documentation Site&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Best for &lt;strong&gt;app-internal narrowing, filtering, and reusable guards&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🤔 Why use &lt;code&gt;is-kit&lt;/code&gt;?&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Tired of rewriting the same &lt;code&gt;isFoo&lt;/code&gt; checks again and again?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; is a good fit when you want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;write reusable &lt;code&gt;isX&lt;/code&gt;&lt;/strong&gt; functions instead of one-off inline checks&lt;/li&gt;
&lt;li&gt;keep runtime validation &lt;strong&gt;lightweight and dependency-free&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;narrow values directly&lt;/strong&gt; in &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, and other TypeScript control flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;compose validation logic&lt;/strong&gt;…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;At first glance, &lt;code&gt;is-kit&lt;/code&gt; and &lt;code&gt;Zod&lt;/code&gt; look similar.&lt;br&gt;&lt;br&gt;
But their &lt;strong&gt;design philosophy is very different&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;is-kit&lt;/code&gt; → a toolkit to &lt;strong&gt;compose type guards&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Zod&lt;/code&gt; → a library to &lt;strong&gt;define schemas and validate data&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of asking &lt;em&gt;“which is better?”&lt;/em&gt;,&lt;br&gt;&lt;br&gt;
let’s compare them from a practical angle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;When is each one easier to write?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We’ll look at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code size&lt;/li&gt;
&lt;li&gt;Readability&lt;/li&gt;
&lt;li&gt;How they handle types&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Same Example
&lt;/h2&gt;

&lt;p&gt;Let’s validate a simple &lt;code&gt;User&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  🔧 is-kit
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;and&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;oneOfValues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;optionalKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;predicateToRefine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is-kit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isPositiveInt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;predicateToRefine&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isPositiveInt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;oneOfValues&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;member&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;nickname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;optionalKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  💎 Zod
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;member&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;nickname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You can already see the difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;is-kit&lt;/code&gt; → compose small guards (function-based)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Zod&lt;/code&gt; → define a schema (declarative)&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  1. Code Size
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Zod&lt;/code&gt; often looks shorter.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because you can stack constraints in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works very well for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forms&lt;/li&gt;
&lt;li&gt;API validation&lt;/li&gt;
&lt;li&gt;Input boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the other hand, &lt;code&gt;is-kit&lt;/code&gt; shines when you want to reuse guards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// already narrowed&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isUser&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can directly pass guards into control flow.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;Zod&lt;/code&gt;, you usually go through &lt;code&gt;safeParse&lt;/code&gt;,&lt;br&gt;
so it feels more like &lt;strong&gt;“calling validation”&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;👉 Summary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Want &lt;strong&gt;centralized validation rules&lt;/strong&gt; → &lt;code&gt;Zod&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Want &lt;strong&gt;reusable guards in logic&lt;/strong&gt; → &lt;code&gt;is-kit&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  2. Readability
&lt;/h2&gt;

&lt;p&gt;This depends on what “readable” means to you.&lt;/p&gt;
&lt;h3&gt;
  
  
  💎 Zod
&lt;/h3&gt;

&lt;p&gt;All rules are in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can instantly see:&lt;/p&gt;

&lt;p&gt;👉 “What shape is valid?”&lt;/p&gt;

&lt;p&gt;Great for boundaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔧 is-kit
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; feels like normal TypeScript control flow.&lt;/p&gt;

&lt;p&gt;It also lets you compose guards step by step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hasKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;narrowKeyTo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is-kit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hasKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hasRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;byRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;narrowKeyTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isAdmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;byRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Narrow the whole shape (&lt;code&gt;isUser&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Focus on a specific key (&lt;code&gt;role&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Narrow further (&lt;code&gt;admin&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This fits naturally with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Receive unknown → narrow step by step”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;👉 Summary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Want &lt;strong&gt;a clear spec of valid data&lt;/strong&gt; → &lt;code&gt;Zod&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Want &lt;strong&gt;natural control flow narrowing&lt;/strong&gt; → &lt;code&gt;is-kit&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. How Types Are Handled
&lt;/h2&gt;

&lt;p&gt;This is where the biggest difference appears.&lt;/p&gt;

&lt;h3&gt;
  
  
  💎 Zod → Schema → Type
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Define schema&lt;/li&gt;
&lt;li&gt;Generate type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Best when you want:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Validation and types in one place”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  🔧 is-kit → Guard → Narrowing
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; focuses on control flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type is narrowed automatically.&lt;/p&gt;

&lt;p&gt;You can also extract the type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GuardedOf&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is-kit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;GuardedOf&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;isUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the main strength is not type generation.&lt;/p&gt;

&lt;p&gt;👉 It’s this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TypeScript understands your logic as you narrow values&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Core Difference
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Zod&lt;/code&gt; → strong at schema → type&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;is-kit&lt;/code&gt; → strong at guard → control flow narrowing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Perspective&lt;/th&gt;
&lt;th&gt;Zod 💎&lt;/th&gt;
&lt;th&gt;is-kit 🔧&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Code size&lt;/td&gt;
&lt;td&gt;Great for schema&lt;/td&gt;
&lt;td&gt;Great for reuse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readability&lt;/td&gt;
&lt;td&gt;Clear validation rules&lt;/td&gt;
&lt;td&gt;Natural control flow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type handling&lt;/td&gt;
&lt;td&gt;Schema-driven types&lt;/td&gt;
&lt;td&gt;Control flow narrowing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  When to Use Each
&lt;/h2&gt;

&lt;p&gt;My approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;Zod&lt;/code&gt; for external inputs

&lt;ul&gt;
&lt;li&gt;API&lt;/li&gt;
&lt;li&gt;forms&lt;/li&gt;
&lt;li&gt;environment variables&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Use &lt;code&gt;is-kit&lt;/code&gt; for internal logic

&lt;ul&gt;
&lt;li&gt;condition branches&lt;/li&gt;
&lt;li&gt;filtering&lt;/li&gt;
&lt;li&gt;composing guards&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  They Are Not Competitors
&lt;/h2&gt;

&lt;p&gt;These tools are not direct competitors.&lt;/p&gt;

&lt;p&gt;They work well together.&lt;/p&gt;

&lt;p&gt;👉 Clean separation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Zod&lt;/code&gt; → validate at boundaries&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;is-kit&lt;/code&gt; → refine inside your app&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; is not a lightweight version of &lt;code&gt;Zod&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It focuses on a different idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Making type guards feel natural in TypeScript&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And &lt;code&gt;Zod&lt;/code&gt; is still extremely strong:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Schema-first validation with type safety&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The real question is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Which one is better?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“Where do you enforce type safety in your design?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Try it if you're interested:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3iya7n6y3u85noozv5bp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3iya7n6y3u85noozv5bp.png" alt=" " width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;https://github.com/nyaomaru/is-kit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>opensource</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building a Browser Game with React: What Doesn’t work well (Run Away From Work)</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Wed, 08 Apr 2026 14:30:28 +0000</pubDate>
      <link>https://dev.to/nyaomaru/building-a-browser-game-with-react-what-doesnt-work-well-run-away-from-work-1khf</link>
      <guid>https://dev.to/nyaomaru/building-a-browser-game-with-react-what-doesnt-work-well-run-away-from-work-1khf</guid>
      <description>&lt;p&gt;Hi!&lt;/p&gt;

&lt;p&gt;I'm &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;@nyaomaru&lt;/a&gt;, a frontend engineer who finally managed to make it to the first zombie in &lt;strong&gt;Resident Evil 9&lt;/strong&gt;. It was terrifying. 🧟🧑‍⚕️&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/BZFz58E4F70"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;A couple of articles ago, I introduced a browser game I built. Have you played it yet?&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://nyaomaru-portfolio.vercel.app/game" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnyaomaru-portfolio.vercel.app%2Fassets%2Fnyaomaru_ogp.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://nyaomaru-portfolio.vercel.app/game" rel="noopener noreferrer" class="c-link"&gt;
            Game - Nyaomaru
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Portfolio of Nyaomaru – A frontend engineer specializing in Vue, React, and TypeScript.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnyaomaru-portfolio.vercel.app%2Ffavicon.svg"&gt;
          nyaomaru-portfolio.vercel.app
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;At first glance, it looks like a very simple game. But once I actually started building it, I ran into a surprising number of tricky problems.&lt;/p&gt;

&lt;p&gt;In this article, I’ll share the main things that tripped me up and how I dealt with them.&lt;/p&gt;

&lt;p&gt;Let’s get into it.&lt;/p&gt;




&lt;h2&gt;
  
  
  🍽️ Quick recap
&lt;/h2&gt;

&lt;p&gt;I built a browser game with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Remix&lt;/code&gt; × &lt;code&gt;React&lt;/code&gt; × &lt;code&gt;TypeScript&lt;/code&gt; × &lt;code&gt;FSD&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you want the full background, check out my previous two articles:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/nyaomaru/i-built-a-run-away-from-work-browser-game-with-react-and-typescript-44ol" class="crayons-story__hidden-navigation-link"&gt;I Built a “Run Away From Work” Browser Game with React and TypeScript&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/nyaomaru" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1436158%2Fa78bc4ed-7796-4760-baa5-06283c2404d6.png" alt="nyaomaru profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/nyaomaru" class="crayons-story__secondary fw-medium m:hidden"&gt;
              nyaomaru
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                nyaomaru
                
              
              &lt;div id="story-author-preview-content-3327084" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/nyaomaru" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1436158%2Fa78bc4ed-7796-4760-baa5-06283c2404d6.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;nyaomaru&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/nyaomaru/i-built-a-run-away-from-work-browser-game-with-react-and-typescript-44ol" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Mar 18&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/nyaomaru/i-built-a-run-away-from-work-browser-game-with-react-and-typescript-44ol" id="article-link-3327084"&gt;
          I Built a “Run Away From Work” Browser Game with React and TypeScript
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gamedev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gamedev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/react"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;react&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/typescript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;typescript&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/nyaomaru/i-built-a-run-away-from-work-browser-game-with-react-and-typescript-44ol" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;28&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/nyaomaru/i-built-a-run-away-from-work-browser-game-with-react-and-typescript-44ol#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              7&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;



&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/nyaomaru/run-away-from-work-stopped-using-react-for-the-game-loop-3e0k" class="crayons-story__hidden-navigation-link"&gt;Run Away From Work — Stopped Using React for the Game Loop&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/nyaomaru" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1436158%2Fa78bc4ed-7796-4760-baa5-06283c2404d6.png" alt="nyaomaru profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/nyaomaru" class="crayons-story__secondary fw-medium m:hidden"&gt;
              nyaomaru
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                nyaomaru
                
              
              &lt;div id="story-author-preview-content-3395758" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/nyaomaru" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1436158%2Fa78bc4ed-7796-4760-baa5-06283c2404d6.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;nyaomaru&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/nyaomaru/run-away-from-work-stopped-using-react-for-the-game-loop-3e0k" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Mar 25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/nyaomaru/run-away-from-work-stopped-using-react-for-the-game-loop-3e0k" id="article-link-3395758"&gt;
          Run Away From Work — Stopped Using React for the Game Loop
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/showdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;showdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gamedev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gamedev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/react"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;react&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/typescript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;typescript&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/nyaomaru/run-away-from-work-stopped-using-react-for-the-game-loop-3e0k" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;20&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/nyaomaru/run-away-from-work-stopped-using-react-for-the-game-loop-3e0k#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              6&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;





&lt;h2&gt;
  
  
  ⛳ What I struggled with
&lt;/h2&gt;

&lt;p&gt;These were the biggest issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;extra whitespace when animating SVG-based sprites&lt;/li&gt;
&lt;li&gt;inconsistent frame behavior across different screen sizes and devices&lt;/li&gt;
&lt;li&gt;AI not generating the CSS animation I actually wanted&lt;/li&gt;
&lt;li&gt;collision detection that didn’t match the visuals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s go through them one by one.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. SVG sprite animation and the whitespace problem
&lt;/h2&gt;

&lt;p&gt;At first, I thought:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“They’re SVGs. Animation should be easy if I just swap them nicely.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It was not easy at all.&lt;/p&gt;

&lt;p&gt;The biggest issue was this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Even when two SVGs looked like they were the same size, their positions shifted during animation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;frame 1 and frame 2 of the running animation had slightly different visual centers&lt;/li&gt;
&lt;li&gt;the boss’s idle state and attack state didn’t line up at the bottom&lt;/li&gt;
&lt;li&gt;even with the same &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt;, the actual rendered position didn’t match perfectly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So from React’s point of view, I was “just swapping images of the same size.”&lt;/p&gt;

&lt;p&gt;But in reality, differences in the SVG &lt;code&gt;viewBox&lt;/code&gt; and internal whitespace made them visibly jump.&lt;/p&gt;

&lt;p&gt;In the end, instead of trying to force the SVG contents to align perfectly, I treated the outer container as fixed and only swapped the sprite inside it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt;
  &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;playerSpriteRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;PLAYER_RUN_SPRITES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"player"&lt;/span&gt;
  &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerSprite&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;draggable&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;aria-hidden&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.playerSprite&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;object-fit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contain&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;object-position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt; &lt;span class="nb"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The key was to align everything to the &lt;strong&gt;bottom&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc15zre6ediuyxcf539t5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc15zre6ediuyxcf539t5.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That alone greatly reduced the weird “bouncing” feeling when frames switched.&lt;/p&gt;

&lt;p&gt;🔑 The takeaway here was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat SVGs not as visuals, but as boxes that include invisible padding.&lt;/strong&gt; 📦&lt;/p&gt;

&lt;p&gt;If you underestimate that, it will absolutely come back to bite you later.&lt;/p&gt;


&lt;h3&gt;
  
  
  2. DOM movement behaving differently depending on display size and device
&lt;/h3&gt;

&lt;p&gt;This was the next problem.&lt;/p&gt;

&lt;p&gt;At first, I used &lt;code&gt;requestAnimationFrame&lt;/code&gt; and moved obstacles left a little bit on every frame.&lt;/p&gt;

&lt;p&gt;That sounds reasonable, but it caused the &lt;strong&gt;game speed to vary depending on the device&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt; does not guarantee perfectly consistent timing.&lt;/p&gt;

&lt;p&gt;That led to problems like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the game felt smooth on desktop&lt;/li&gt;
&lt;li&gt;obstacles looked slower on mobile when FPS dropped&lt;/li&gt;
&lt;li&gt;differences in screen size and rendering load changed the perceived difficulty&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I left that alone, the exact same code would create effectively different games on different devices.&lt;/p&gt;

&lt;p&gt;So instead of moving things by a fixed amount per frame, I switched to time-based movement.&lt;/p&gt;

&lt;p&gt;The important thing here was using &lt;code&gt;deltaTime&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;deltaTime&lt;/code&gt; represents how many milliseconds passed between the previous frame and the current one.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;at 60fps, one frame is about &lt;code&gt;16.6ms&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;at 30fps, one frame is about &lt;code&gt;33.3ms&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So when FPS drops, each frame covers more time.&lt;/p&gt;

&lt;p&gt;If you move an obstacle by the same number of pixels every frame, then lower-FPS devices will make the obstacle move more slowly overall.&lt;/p&gt;

&lt;p&gt;But with &lt;code&gt;deltaTime&lt;/code&gt;, you can adjust movement based on elapsed time.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frameDistancePx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obstacleSpeedPxPerSec&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;deltaTimeMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;frameDistancePx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This made it much easier to keep movement consistent on a per-second basis, even when FPS fluctuated.&lt;/p&gt;

&lt;p&gt;That said, &lt;code&gt;deltaTime&lt;/code&gt; alone was not enough to fully close the gap.&lt;/p&gt;

&lt;p&gt;So after switching to time-based movement, I also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;used different speed constants for mobile and desktop&lt;/li&gt;
&lt;li&gt;applied an additional pace scale only on large desktop screens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That let me preserve the original feel on desktop without making mobile gameplay too slow or too heavy.&lt;/p&gt;

&lt;p&gt;🔑 The takeaway here was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Game logic should be designed around time, not frames.&lt;/strong&gt; 🕛&lt;/p&gt;

&lt;p&gt;Browser games may look simple, but if you ignore device differences, the overall quality drops fast.&lt;/p&gt;


&lt;h3&gt;
  
  
  3. AI couldn’t generate the CSS animation I actually wanted
&lt;/h3&gt;

&lt;p&gt;This one was subtle, but painful.&lt;/p&gt;

&lt;p&gt;For some effects, like the clear animation, I needed something with these characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the image changes midway&lt;/li&gt;
&lt;li&gt;the starting position is only known at runtime&lt;/li&gt;
&lt;li&gt;the duration also changes depending on the situation&lt;/li&gt;
&lt;li&gt;but the overall animation shape should stay fixed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kind of thing did not go well with &lt;code&gt;codex&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It really struggled with animations that included dynamic values like runtime position and duration.&lt;/p&gt;

&lt;p&gt;So I ended up handling this part myself.&lt;/p&gt;

&lt;p&gt;At first, I tried doing everything inline.&lt;/p&gt;

&lt;p&gt;But once I started pushing even &lt;code&gt;@keyframes&lt;/code&gt;-related concerns into the component, the responsibilities between rendering and visual behavior got messy very quickly.&lt;/p&gt;

&lt;p&gt;So I split it like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fixed animation definitions live in CSS Modules&lt;/li&gt;
&lt;li&gt;only dynamic values like position and duration are passed via CSS variables
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;
  &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flyoutMotion&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--flyout-origin-x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;specialFlyoutOrigin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--flyout-origin-y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;specialFlyoutOrigin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--flyout-duration-ms&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;specialFlyoutDurationMs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;CSSProperties&lt;/span&gt;
  &lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.flyoutMotion&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--flyout-origin-x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--flyout-origin-y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;special-flyout-motion&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--flyout-duration-ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;forwards&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;From there, I fine-tuned the easing with &lt;code&gt;cubic-bezier&lt;/code&gt; and adjusted the actual motion in the browser, kind of like shaping keyframes in After Effects.&lt;/p&gt;

&lt;p&gt;🔑 The takeaway here was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep fixed animation logic in CSS, and pass only dynamic values through CSS variables.&lt;/strong&gt; 🫱&lt;/p&gt;

&lt;p&gt;That separation made the animation code much easier to reason about and tweak later.&lt;/p&gt;


&lt;h3&gt;
  
  
  4. Collision detection didn’t match the visuals
&lt;/h3&gt;

&lt;p&gt;And finally, collision detection.&lt;/p&gt;

&lt;p&gt;This is always painful in browser games, and this project was no exception.&lt;/p&gt;

&lt;p&gt;The main issue was this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The visual shape and the actual collision area did not match.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I naïvely used &lt;code&gt;getBoundingClientRect()&lt;/code&gt; against everything, I ran into problems like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;collisions triggering even when it didn’t look like the player touched anything&lt;/li&gt;
&lt;li&gt;obvious hits sometimes slipping through&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reasons were mainly these two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the visible SVG shape didn’t match its rectangular bounds&lt;/li&gt;
&lt;li&gt;the “visually correct” size and the “feels fair in gameplay” size were different for each obstacle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, desk-type obstacles had a large visual box, so using the raw rectangle made collisions feel unfair.&lt;/p&gt;

&lt;p&gt;Here’s what the desk obstacle looked like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftzovv7ny344fdvveldk5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftzovv7ny344fdvveldk5.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So I separated visual size from hitbox size.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;obstacleElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hitboxScale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;resolvedHitboxScale&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hitboxScale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hitboxScale&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;obstacleHitbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getScaledHitboxFromRectLike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obstacleBox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hitboxScale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isCollision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;playerRect&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;isPlayerOverlappingHitbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playerRect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;obstacleHitbox&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That way, the obstacle could stay visually large and impactful, while the collision box could be slightly reduced to feel fair.&lt;/p&gt;

&lt;p&gt;This also made balancing much easier, because I could tweak hitbox scale per obstacle.&lt;/p&gt;

&lt;p&gt;🔑 The takeaway here was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visuals and collision should be designed separately.&lt;/strong&gt; 🛠️&lt;/p&gt;

&lt;p&gt;This was probably the part that made me feel the most like, “Okay, now I’m actually building a game.”&lt;/p&gt;


&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The game looks simple, but the implementation involved a lot of careful decisions.&lt;/p&gt;

&lt;p&gt;The biggest lessons were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;treat SVGs as boxes that include whitespace&lt;/li&gt;
&lt;li&gt;design movement based on time, not frames&lt;/li&gt;
&lt;li&gt;separate CSS and JS responsibilities clearly&lt;/li&gt;
&lt;li&gt;separate visuals and hit detection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Getting these details right makes a huge difference in how polished the game feels.&lt;/p&gt;

&lt;p&gt;So if you’re thinking about building a browser game, I’d really recommend paying attention to these parts from the beginning. 🚀&lt;/p&gt;

&lt;p&gt;Also, the repository is public, so feel free to take a look:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;
        nyaomaru
      &lt;/a&gt; / &lt;a href="https://github.com/nyaomaru/nyaomaru-portfolio" rel="noopener noreferrer"&gt;
        nyaomaru-portfolio
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      "Run Away From Work" Game is here → https://nyaomaru-portfolio.vercel.app/game
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Nyaomaru Portfolio&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;
    &lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/nyaomaru/nyaomaru-portfolio/main/public/assets/nyaomaru_game_thumbnail.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fnyaomaru-portfolio%2Fmain%2Fpublic%2Fassets%2Fnyaomaru_game_thumbnail.png" width="600px" alt="nyaomaru run game thumbnail"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;"Run Away From Work"&lt;/p&gt;

&lt;p&gt;You can play here: &lt;a href="https://nyaomaru-portfolio.vercel.app/game" rel="nofollow noopener noreferrer"&gt;https://nyaomaru-portfolio.vercel.app/game&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;Remix&lt;/code&gt; + &lt;code&gt;React&lt;/code&gt; + &lt;code&gt;TypeScript&lt;/code&gt; portfolio built with Feature-Sliced Design.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://feature-sliced.github.io/documentation/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/7957ab11c8d162af98add2597ff870e7c08725f501980553160128b7938f9a6b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f466561747572652d2d536c696365642d44657369676e3f7374796c653d666f722d7468652d626164676526636f6c6f723d463246324632266c6162656c436f6c6f723d323632323234266c6f676f57696474683d3130266c6f676f3d646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e53556845556741414142514141414161434159414141433367337839414141414358424957584d4141414c46414141437851474a316e2f764141414141584e535230494172733463365141414141526e51553142414143786a7776385951554141414249535552425648674237644b784351416744455452307732637773306379733263776845554262736767696b437556656b4448775351466c596f37512b384b6e6d74486446574d646b32636c35775373627847535a7738646d387058395a4855544d4255674755324637313841414141415355564f524b35435949493d" alt="shields-fsd-domain"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🚀 Highlights&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jump Game (Main Feature)&lt;/strong&gt;: Side-scrolling jump game with obstacles, boss phases, clear sequences, and restart flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive Terminal&lt;/strong&gt;: Ask profile-related questions in a terminal-style UI backed by the &lt;code&gt;/api/ask&lt;/code&gt; endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responsive UI&lt;/strong&gt;: Works across desktop and mobile layouts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FSD Architecture&lt;/strong&gt;: Organized by features/widgets/pages/shared layers.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🎮 Main Feature: Game&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;
    &lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/nyaomaru/nyaomaru-portfolio/main/public/assets/gif/run_away_from_work.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fnyaomaru-portfolio%2Fmain%2Fpublic%2Fassets%2Fgif%2Frun_away_from_work.gif" width="600px" alt="nyaomaru run game gif"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;The game is available at &lt;code&gt;/game&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Can you watch true ending? 🚀&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;🕹️ Controls&lt;/h3&gt;

&lt;/div&gt;

&lt;p&gt;&lt;/p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;table&gt;

&lt;thead&gt;

&lt;tr&gt;

&lt;th&gt;Action&lt;/th&gt;

&lt;th&gt;Description&lt;/th&gt;

&lt;/tr&gt;

&lt;/thead&gt;

&lt;tbody&gt;

&lt;tr&gt;

&lt;td&gt;Space / Click&lt;/td&gt;

&lt;td&gt;Jump&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;Tap&lt;/td&gt;

&lt;td&gt;Jump&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;Double Jump&lt;/td&gt;

&lt;td&gt;Jump again while in the air&lt;/td&gt;

&lt;/tr&gt;

&lt;/tbody&gt;

&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;💻 Terminal&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;The terminal is available on the top page and is designed for profile Q&amp;amp;A.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;✨ What It Does&lt;/h3&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Sends user input to &lt;code&gt;/api/ask&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Returns an AI-generated answer based on profile context.&lt;/li&gt;
&lt;li&gt;Shows typing/waiting states in terminal history.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;
⚠️ Important&lt;/h3&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;/api/ask&lt;/code&gt;…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nyaomaru/nyaomaru-portfolio" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;





&lt;h2&gt;
  
  
  Bonus: Sound effects matter too
&lt;/h2&gt;

&lt;p&gt;One thing that turned out to be more important than I expected was sound. 🔊&lt;/p&gt;

&lt;p&gt;Audio files need to be lightweight. Even a small delay can hurt the feel of the game.&lt;/p&gt;

&lt;p&gt;I originally exported my sound effects from &lt;code&gt;GarageBand&lt;/code&gt; as &lt;code&gt;wav&lt;/code&gt; files, but the file sizes were too large and the game started to feel noticeably heavier.&lt;/p&gt;

&lt;p&gt;So I used &lt;code&gt;ffmpeg&lt;/code&gt; to convert them to &lt;code&gt;ogg&lt;/code&gt; and reduce the size.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.wav &lt;span class="nt"&gt;-ac&lt;/span&gt; 1 &lt;span class="nt"&gt;-ar&lt;/span&gt; 22050 &lt;span class="nt"&gt;-b&lt;/span&gt;:a 64k output.ogg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After converting them like this, things got much lighter.&lt;/p&gt;

&lt;p&gt;And just to be safe, I also recommend preparing an &lt;code&gt;mp3&lt;/code&gt; fallback.&lt;/p&gt;

&lt;p&gt;So the takeaway here was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small sound files matter. A lot. If you care about game feel, optimize audio too.&lt;/strong&gt; 👍&lt;/p&gt;

&lt;p&gt;Happy coding! 💻&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>typescript</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Run Away From Work — Stopped Using React for the Game Loop</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Wed, 25 Mar 2026 13:11:16 +0000</pubDate>
      <link>https://dev.to/nyaomaru/run-away-from-work-stopped-using-react-for-the-game-loop-3e0k</link>
      <guid>https://dev.to/nyaomaru/run-away-from-work-stopped-using-react-for-the-game-loop-3e0k</guid>
      <description>&lt;p&gt;&lt;a href="https://nyaomaru-portfolio.vercel.app/game" rel="noopener noreferrer"&gt;Play the game here&lt;/a&gt; 👈&lt;/p&gt;

&lt;p&gt;Hi there!&lt;/p&gt;

&lt;p&gt;I’m &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;@nyaomaru&lt;/a&gt;, a frontend engineer who got &lt;em&gt;Resident Evil 9&lt;/em&gt; from NVIDIA and is still too scared to make it to the first zombie. 🧟&lt;/p&gt;

&lt;p&gt;In my previous article, I introduced my small browser game, &lt;strong&gt;Run Away From Work&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At first glance, it looks like a very simple game.&lt;br&gt;&lt;br&gt;
But while building it, I realized something important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you design the game around &lt;code&gt;setState&lt;/code&gt; on every frame, it falls apart very quickly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every frame becomes:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;setState&lt;/code&gt; → re-render → diff → DOM update&lt;/p&gt;

&lt;p&gt;And if you try to do that at 60fps, it gets heavy fast.&lt;/p&gt;

&lt;p&gt;That does &lt;strong&gt;not&lt;/strong&gt; mean React is bad for games.&lt;/p&gt;

&lt;p&gt;It means this is the wrong place to use React’s rendering model.&lt;/p&gt;

&lt;p&gt;So in this article, I want to share how I changed the architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I used &lt;strong&gt;React for the structure&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;and &lt;strong&gt;direct DOM manipulation for high-frequency game updates&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  🐾 Quick recap
&lt;/h2&gt;

&lt;p&gt;This game was built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Remix&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;React&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TypeScript&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Feature-Sliced Design&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want the overview of the project itself, here is the previous article:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/nyaomaru/i-built-a-run-away-from-work-browser-game-with-react-and-typescript-44ol" class="crayons-story__hidden-navigation-link"&gt;I Built a “Run Away From Work” Browser Game with React and TypeScript&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/nyaomaru" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1436158%2Fa78bc4ed-7796-4760-baa5-06283c2404d6.png" alt="nyaomaru profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/nyaomaru" class="crayons-story__secondary fw-medium m:hidden"&gt;
              nyaomaru
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                nyaomaru
                
              
              &lt;div id="story-author-preview-content-3327084" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/nyaomaru" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1436158%2Fa78bc4ed-7796-4760-baa5-06283c2404d6.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;nyaomaru&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/nyaomaru/i-built-a-run-away-from-work-browser-game-with-react-and-typescript-44ol" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Mar 18&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/nyaomaru/i-built-a-run-away-from-work-browser-game-with-react-and-typescript-44ol" id="article-link-3327084"&gt;
          I Built a “Run Away From Work” Browser Game with React and TypeScript
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gamedev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gamedev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/react"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;react&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/typescript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;typescript&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/nyaomaru/i-built-a-run-away-from-work-browser-game-with-react-and-typescript-44ol" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;28&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/nyaomaru/i-built-a-run-away-from-work-browser-game-with-react-and-typescript-44ol#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              7&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;Even though this game is built in React, the most important part of the runtime behavior is &lt;strong&gt;not really handled by React&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because the mental model of React and the mental model of a game loop are very different.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✏️ What this article focuses on
&lt;/h2&gt;

&lt;p&gt;Last time, I wrote more about &lt;em&gt;why&lt;/em&gt; I made the game.&lt;/p&gt;

&lt;p&gt;This time, I want to focus more on the &lt;em&gt;how&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;There are 3 points I especially want to show:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I used &lt;strong&gt;plain DOM rendering&lt;/strong&gt;, not &lt;code&gt;Canvas&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;I split the game logic into &lt;strong&gt;small hooks with clear responsibilities&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;I added several small &lt;strong&gt;optimizations&lt;/strong&gt; to make it feel smooth in the browser&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;All the visuals in the game are SVGs I made myself in Adobe Illustrator.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🥣 React as the shell, DOM as the runtime
&lt;/h2&gt;

&lt;p&gt;This game looks like a React app, but I did &lt;strong&gt;not&lt;/strong&gt; build it in a way that re-renders everything every frame.&lt;/p&gt;

&lt;p&gt;On the React side, I only render the stable structure of the scene.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ROOT_CLASS_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;refs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gameRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;GAME_AREA_CLASS_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FishCounterOverlay&lt;/span&gt;
        &lt;span class="na"&gt;fishCount&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;fishCount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;fishCounterIconSrc&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;fishCounterDisplayIconSrc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PlayerLayer&lt;/span&gt;
        &lt;span class="na"&gt;playerRef&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;refs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;playerSpriteRef&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;refs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerSpriteRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;playerStyle&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;playerStyle&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BossLayer&lt;/span&gt;
        &lt;span class="na"&gt;shouldRenderBoss&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;shouldRenderBoss&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;bossRef&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;refs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bossRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;bossSpriteRef&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;refs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bossSpriteRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;bossArmRef&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;refs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bossArmRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;bossStyle&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;bossStyle&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;bossArmStyle&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;bossArmStyle&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here, React owns the static layers of the game:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;player&lt;/li&gt;
&lt;li&gt;boss&lt;/li&gt;
&lt;li&gt;UI overlay&lt;/li&gt;
&lt;li&gt;base scene structure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But moving entities like obstacles and fish are not stored in React state.&lt;/p&gt;

&lt;p&gt;Instead, I create DOM nodes directly when needed.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spawnObstacle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;obs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;img&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;OBSTACLE_ICON_SOURCES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;iconIndex&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nx"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;absolute&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;initializeMovingEntityMotion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;getSpawnLeft&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="nx"&gt;gameRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;obstaclesRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The reason is simple:&lt;/p&gt;

&lt;p&gt;If I store side-scrolling obstacles in a React array state and update them frame by frame, &lt;strong&gt;the update cost becomes unnecessarily high.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;React is great at building and updating UI structure.&lt;/p&gt;

&lt;p&gt;But for game-like behavior where positions change dozens of times per second, directly manipulating the DOM through &lt;code&gt;refs&lt;/code&gt; felt much more natural.&lt;/p&gt;

&lt;p&gt;It also made the behavior easier to trace and debug.&lt;/p&gt;


&lt;h2&gt;
  
  
  🍴 React and games optimize for different things
&lt;/h2&gt;

&lt;p&gt;React is based on this model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;state changes&lt;/li&gt;
&lt;li&gt;re-render&lt;/li&gt;
&lt;li&gt;reconcile&lt;/li&gt;
&lt;li&gt;apply DOM updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Games are usually based on this model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;update positions every frame&lt;/li&gt;
&lt;li&gt;move objects every frame&lt;/li&gt;
&lt;li&gt;detect collisions every frame&lt;/li&gt;
&lt;li&gt;advance the simulation continuously&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not the same thing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp272o6he6bjjbrmdyuw7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp272o6he6bjjbrmdyuw7.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So in this project, I separated the responsibilities like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React for layout and UI&lt;/li&gt;
&lt;li&gt;imperative DOM updates for high-frequency motion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation made the architecture much easier to work with.&lt;/p&gt;
&lt;h3&gt;
  
  
  What I stopped doing
&lt;/h3&gt;

&lt;p&gt;At first, I tried to manage everything with &lt;code&gt;useState&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That quickly led to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;re-rendering on every frame&lt;/li&gt;
&lt;li&gt;unnecessary diffing&lt;/li&gt;
&lt;li&gt;worse performance&lt;/li&gt;
&lt;li&gt;visibly choppy movement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So halfway through development, I changed the design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I stopped trying to make the entire game fully declarative.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  ✂️ Splitting game logic into hooks
&lt;/h2&gt;

&lt;p&gt;The hub of the whole game scene is &lt;code&gt;useJumpGameScene&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;gameOver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setGameOver&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;showBoss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setShowBoss&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fishCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFishCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;jump&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isOnGroundRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resetJumpState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updateJumpFrame&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nf"&gt;useJump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playerRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;obstaclesRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spawnObstacle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spawnFish&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clearObstacles&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nf"&gt;useObstacles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gameRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resetPlayerSpriteState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updatePlayerSpriteFrame&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nf"&gt;usePlayerSpriteAnimator&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;playerSpriteRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;gameOver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;isOnGroundRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;useGameLoop&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;gameOver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;bossRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;playerRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;obstaclesRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;gameRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;updateJumpFrame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;updatePlayerSpriteFrame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;setGameOver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;setShowBoss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;setGameOverIcon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here is the rough responsibility split:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hook&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useJump&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jump behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useObstacles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Spawn and remove obstacles / fish&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;usePlayerSpriteAnimator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Switch between running / jumping sprites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useGameLoop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-frame progression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useJumpGameScene&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Compose everything and expose values to the UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key idea is this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use &lt;code&gt;useState&lt;/code&gt; only for values the UI actually needs to render&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;useRef&lt;/code&gt; for fast-changing internal state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, the jump behavior looks like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jumpMotionPhaseRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;JumpMotionPhase&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updateJumpFrame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;nowMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deltaTimeMs&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jumpMotionPhaseRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ascending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;posRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;jumpMaxHeightRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;posRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ASCENT_VELOCITY_PX_PER_MS&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;deltaTimeMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;playerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;posRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jumpMotionPhaseRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;descending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;posRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;posRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;descentVelocityPxPerMsRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;deltaTimeMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;playerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;posRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This means the jump position is updated &lt;strong&gt;without going through React rendering.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of storing the player’s vertical position in state and re-rendering on every frame, I keep it in a ref and write it directly to the DOM.&lt;/p&gt;

&lt;p&gt;That tradeoff worked much better for this kind of game behavior.&lt;/p&gt;


&lt;h2&gt;
  
  
  🧵 One game loop controls the clock
&lt;/h2&gt;

&lt;p&gt;The game progression runs on top of &lt;code&gt;requestAnimationFrame&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nowMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getFrameTiming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nowMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastFrameAtMsRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;lastFrameAtMsRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;timing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nowMs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;updateJumpFrame&lt;/span&gt;&lt;span class="p"&gt;?.({&lt;/span&gt; &lt;span class="na"&gt;nowMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nowMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;deltaTimeMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deltaTimeMs&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;updatePlayerSpriteFrame&lt;/span&gt;&lt;span class="p"&gt;?.({&lt;/span&gt; &lt;span class="na"&gt;nowMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nowMs&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fatalCollisionIcon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;updateObstaclesFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;deltaTimeMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deltaTimeMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;obstacleSpeedPxPerSec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;obstaclesRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;playerRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;getGameWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;getPlayerRect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;playerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;getGameRect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;gameRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fatalCollisionIcon&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;triggerFault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fatalCollisionIcon&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;animationId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This loop handles things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;jump updates&lt;/li&gt;
&lt;li&gt;sprite animation updates&lt;/li&gt;
&lt;li&gt;obstacle movement&lt;/li&gt;
&lt;li&gt;collision checks&lt;/li&gt;
&lt;li&gt;boss appearance&lt;/li&gt;
&lt;li&gt;clear / fail transitions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;code&gt;useGameLoop&lt;/code&gt; acts as the central clock of the game, while the other hooks behave like specialized modules plugged into that clock.&lt;/p&gt;

&lt;p&gt;That structure helped me keep the logic separated without scattering time-related behavior everywhere.&lt;/p&gt;


&lt;h2&gt;
  
  
  🕶 Small optimizations mattered a lot
&lt;/h2&gt;

&lt;p&gt;A lot of the smoothness came from tiny DOM-side optimizations.&lt;/p&gt;
&lt;h3&gt;
  
  
  Moving elements with transform
&lt;/h3&gt;

&lt;p&gt;For movement, I avoided updating &lt;code&gt;left&lt;/code&gt; every frame.&lt;/p&gt;

&lt;p&gt;Instead, I used &lt;code&gt;transform: translate3d(...)&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;initializeMovingEntityMotion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;spawnLeftPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;spawnLeftPx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spawnLeftPx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;spawnLeftPx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;translateXPx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`translate3d(0px, 0px, 0px)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;willChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transform&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;advanceMovingEntityMotion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;frameDistancePx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextTranslateXPx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;translateXPx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;frameDistancePx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;translateXPx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nextTranslateXPx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`translate3d(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nextTranslateXPx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px, 0px, 0px)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This helps avoid unnecessary layout work and gives the browser a stronger hint that the element is going to move.&lt;/p&gt;
&lt;h3&gt;
  
  
  Avoiding unnecessary sprite swaps
&lt;/h3&gt;

&lt;p&gt;I also avoided changing the player sprite image more than necessary.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preload sprite assets in advance&lt;/li&gt;
&lt;li&gt;skip updates if the current sprite is already correct&lt;/li&gt;
&lt;li&gt;briefly keep the same sprite after landing to make the animation feel better
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;currentPlayerSpriteRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;spritePath&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nf"&gt;isSameSpriteSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playerSpriteRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spritePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;preloadedPlayerSprites&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;spritePath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;applySprite&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This kind of thing is small, but it affects the feeling of responsiveness more than I expected.&lt;/p&gt;
&lt;h3&gt;
  
  
  Only calculating when needed
&lt;/h3&gt;

&lt;p&gt;Collision handling also avoids doing full &lt;code&gt;getBoundingClientRect()&lt;/code&gt; calls every single time.&lt;/p&gt;

&lt;p&gt;I cache width, height, and bottom values in &lt;code&gt;dataset&lt;/code&gt; when entities are created, then reconstruct their rectangle when possible.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;obstacleBox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;resolvedGameRect&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getObstacleRectFromCachedLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentLeftPx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolvedGameRect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
      &lt;span class="nx"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And I do not even start collision checks until the obstacle is close enough to the player.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentLeftPx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;gameWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;OBSTACLE_PLAYER_PROXIMITY_CHECK_PX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;resolvedPlayerBox&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="nf"&gt;getPlayerRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;resolvedGameRect&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="nf"&gt;getGameRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// collision logic starts here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That idea was very effective.&lt;/p&gt;

&lt;p&gt;There is no reason to do precise collision work for an obstacle that is still far away on the right side of the screen.&lt;/p&gt;

&lt;p&gt;That kind of selective work made the movement noticeably smoother.&lt;/p&gt;


&lt;h2&gt;
  
  
  🎯 Final thoughts
&lt;/h2&gt;

&lt;p&gt;The main lessons from this implementation were:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use React for layout and UI&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;refs&lt;/code&gt; and direct DOM manipulation for high-frequency updates&lt;/li&gt;
&lt;li&gt;Split the logic into &lt;code&gt;hooks&lt;/code&gt; with focused responsibilities&lt;/li&gt;
&lt;li&gt;Stack small optimizations like &lt;code&gt;transforms&lt;/code&gt;, &lt;code&gt;cached rects&lt;/code&gt;, &lt;code&gt;proximity-based collision checks&lt;/code&gt;, and &lt;code&gt;sprite preloading&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the conclusion is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;React is bad for browser games&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is closer to this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;React is not a great place to run your core game loop.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For this project, the best balance was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;declarative structure&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;imperative runtime behavior&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I build a different kind of game in the future, I might choose &lt;code&gt;Canvas&lt;/code&gt; or &lt;code&gt;WebGL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But for a small game embedded naturally inside a portfolio site, I found that &lt;strong&gt;a DOM-based approach worked surprisingly well.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Bonus
&lt;/h2&gt;

&lt;p&gt;If you liked the game, I would love it if you starred the repository ⭐:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;
        nyaomaru
      &lt;/a&gt; / &lt;a href="https://github.com/nyaomaru/nyaomaru-portfolio" rel="noopener noreferrer"&gt;
        nyaomaru-portfolio
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      "Run Away From Work" Game is here → https://nyaomaru-portfolio.vercel.app/game
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Nyaomaru Portfolio&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;
    &lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/nyaomaru/nyaomaru-portfolio/main/public/assets/nyaomaru_game_thumbnail.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fnyaomaru-portfolio%2Fmain%2Fpublic%2Fassets%2Fnyaomaru_game_thumbnail.png" width="600px" alt="nyaomaru run game thumbnail"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;"Run Away From Work"&lt;/p&gt;

&lt;p&gt;You can play here: &lt;a href="https://nyaomaru-portfolio.vercel.app/game" rel="nofollow noopener noreferrer"&gt;https://nyaomaru-portfolio.vercel.app/game&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;Remix&lt;/code&gt; + &lt;code&gt;React&lt;/code&gt; + &lt;code&gt;TypeScript&lt;/code&gt; portfolio built with Feature-Sliced Design.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://feature-sliced.github.io/documentation/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/7957ab11c8d162af98add2597ff870e7c08725f501980553160128b7938f9a6b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f466561747572652d2d536c696365642d44657369676e3f7374796c653d666f722d7468652d626164676526636f6c6f723d463246324632266c6162656c436f6c6f723d323632323234266c6f676f57696474683d3130266c6f676f3d646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e53556845556741414142514141414161434159414141433367337839414141414358424957584d4141414c46414141437851474a316e2f764141414141584e535230494172733463365141414141526e51553142414143786a7776385951554141414249535552425648674237644b784351416744455452307732637773306379733263776845554262736767696b437556656b4448775351466c596f37512b384b6e6d74486446574d646b32636c35775373627847535a7738646d387058395a4855544d4255674755324637313841414141415355564f524b35435949493d" alt="shields-fsd-domain"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🚀 Highlights&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jump Game (Main Feature)&lt;/strong&gt;: Side-scrolling jump game with obstacles, boss phases, clear sequences, and restart flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive Terminal&lt;/strong&gt;: Ask profile-related questions in a terminal-style UI backed by the &lt;code&gt;/api/ask&lt;/code&gt; endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responsive UI&lt;/strong&gt;: Works across desktop and mobile layouts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FSD Architecture&lt;/strong&gt;: Organized by features/widgets/pages/shared layers.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🎮 Main Feature: Game&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;
    &lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/nyaomaru/nyaomaru-portfolio/main/public/assets/gif/run_away_from_work.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fnyaomaru%2Fnyaomaru-portfolio%2Fmain%2Fpublic%2Fassets%2Fgif%2Frun_away_from_work.gif" width="600px" alt="nyaomaru run game gif"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;The game is available at &lt;code&gt;/game&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Can you watch true ending? 🚀&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;🕹️ Controls&lt;/h3&gt;

&lt;/div&gt;

&lt;p&gt;&lt;/p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;table&gt;

&lt;thead&gt;

&lt;tr&gt;

&lt;th&gt;Action&lt;/th&gt;

&lt;th&gt;Description&lt;/th&gt;

&lt;/tr&gt;

&lt;/thead&gt;

&lt;tbody&gt;

&lt;tr&gt;

&lt;td&gt;Space / Click&lt;/td&gt;

&lt;td&gt;Jump&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;Tap&lt;/td&gt;

&lt;td&gt;Jump&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;Double Jump&lt;/td&gt;

&lt;td&gt;Jump again while in the air&lt;/td&gt;

&lt;/tr&gt;

&lt;/tbody&gt;

&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;💻 Terminal&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;The terminal is available on the top page and is designed for profile Q&amp;amp;A.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;✨ What It Does&lt;/h3&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Sends user input to &lt;code&gt;/api/ask&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Returns an AI-generated answer based on profile context.&lt;/li&gt;
&lt;li&gt;Shows typing/waiting states in terminal history.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;
⚠️ Important&lt;/h3&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;/api/ask&lt;/code&gt;…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nyaomaru/nyaomaru-portfolio" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;You can also play it on Itch:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://nyaomaru.itch.io/run-away-from-work" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;nyaomaru.itch.io&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>showdev</category>
      <category>gamedev</category>
      <category>react</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
