<?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.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>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="420" 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://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" width="873" height="862"&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" 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-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="" 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/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="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;26&lt;span class="hidden s:inline"&gt; 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;
              Comments


              7&lt;span class="hidden s:inline"&gt; 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" 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-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="" 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/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="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;18&lt;span class="hidden s:inline"&gt; 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;
              Comments


              6&lt;span class="hidden s:inline"&gt; 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=" " width="800" height="450"&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=" " width="800" height="450"&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;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;br&gt;
&lt;thead&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;br&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;/thead&gt;
&lt;br&gt;
&lt;tbody&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;Space / Click&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Jump&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;Tap&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Jump&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;Double Jump&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Jump again while in the air&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;/tbody&gt;
&lt;br&gt;
&lt;/table&gt;&lt;/div&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;26&lt;span class="hidden s:inline"&gt; 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;
              Comments


              7&lt;span class="hidden s:inline"&gt; 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;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;br&gt;
&lt;thead&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;br&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;/thead&gt;
&lt;br&gt;
&lt;tbody&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;Space / Click&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Jump&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;Tap&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Jump&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;Double Jump&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Jump again while in the air&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;/tbody&gt;
&lt;br&gt;
&lt;/table&gt;&lt;/div&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>
    <item>
      <title>I Built a “Run Away From Work” Browser Game with React and TypeScript</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Wed, 18 Mar 2026 12:14:00 +0000</pubDate>
      <link>https://dev.to/nyaomaru/i-built-a-run-away-from-work-browser-game-with-react-and-typescript-44ol</link>
      <guid>https://dev.to/nyaomaru/i-built-a-run-away-from-work-browser-game-with-react-and-typescript-44ol</guid>
      <description>&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%2F2k03xsh3hb98i11p6621.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%2F2k03xsh3hb98i11p6621.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click Game Button 👇&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nyaomaru-portfolio.vercel.app/game" 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.amazonaws.com%2Fuploads%2Farticles%2Fy8hodn5dx2dlrtolo8bd.png" alt=" " width="200" height="71"&gt;&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 a cold after getting way too excited about spring and spending too much time in the sun. ☀️&lt;/p&gt;

&lt;p&gt;Thanks to AI, we can get work done faster than ever and handle a huge number of tasks in parallel.&lt;/p&gt;

&lt;p&gt;But at the same time… dealing with more and more work can get exhausting, right? 😿&lt;/p&gt;

&lt;p&gt;So I made a small &lt;strong&gt;browser game about running away from work&lt;/strong&gt; as a little break.&lt;/p&gt;

&lt;p&gt;Here’s what it looks 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%2F9hvrz7vom3793fuemh8q.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%2F9hvrz7vom3793fuemh8q.gif" alt=" " width="760" height="304"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://nyaomaru-portfolio.vercel.app/game" rel="noopener noreferrer"&gt;&lt;strong&gt;Play here&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It runs in the browser, so you can play it on both desktop and mobile.&lt;br&gt;&lt;br&gt;
It takes about &lt;strong&gt;1 minute&lt;/strong&gt; to play.&lt;/p&gt;

&lt;p&gt;Hope it gives you a small break 🙏&lt;/p&gt;


&lt;h2&gt;
  
  
  🎮 How to play
&lt;/h2&gt;

&lt;p&gt;The controls are simple.&lt;/p&gt;

&lt;p&gt;On desktop, press &lt;strong&gt;Space&lt;/strong&gt; or &lt;strong&gt;Click&lt;/strong&gt; to jump.&lt;br&gt;&lt;br&gt;
On mobile, just &lt;strong&gt;Tap&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can even &lt;strong&gt;double jump&lt;/strong&gt;, so try to dodge everything nicely.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&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 once more in the air&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;🐟 If you collect fish, something good might happen... maybe!!!???&lt;/p&gt;


&lt;h2&gt;
  
  
  🤔 Why I made this
&lt;/h2&gt;

&lt;p&gt;There were two main reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I wanted to study &lt;strong&gt;UI/UX&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;I wanted to see how far I could go using the technologies I’m already good at&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m planning to get more serious about game development with &lt;code&gt;Unity&lt;/code&gt; in the future, but before that, I wanted to try making a browser game with my current stack: &lt;strong&gt;&lt;code&gt;React&lt;/code&gt; × &lt;code&gt;TypeScript&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Also, do you know Google’s 404 &lt;a href="https://trex-runner.com/" rel="noopener noreferrer"&gt;Dinosaur T-Rex Game&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;I’ve always liked how simple and fun it is.&lt;/p&gt;

&lt;p&gt;That made me want to build my own &lt;strong&gt;fun little runner game&lt;/strong&gt;.&lt;/p&gt;

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


&lt;h2&gt;
  
  
  ⚙️ How I built it
&lt;/h2&gt;

&lt;p&gt;I had previously built my portfolio with &lt;code&gt;Remix&lt;/code&gt; × &lt;code&gt;FSD&lt;/code&gt;, so this game was made as an extension of that idea.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/nyaomaru/building-a-portfolio-site-with-fsd-langchain-remix-ai-c9h"&gt;https://dev.to/nyaomaru/building-a-portfolio-site-with-fsd-langchain-remix-ai-c9h&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the stack is basically:&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;FSD&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why this structure?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Games tend to have &lt;strong&gt;a lot of state&lt;/strong&gt;, so I wanted to test how far I could manage things with &lt;code&gt;hooks&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;I wanted the codebase to stay &lt;strong&gt;type-safe&lt;/strong&gt; and maintainable&lt;/li&gt;
&lt;li&gt;I wanted to test the &lt;strong&gt;scalability of FSD&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;I wanted to include it in my portfolio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ll probably write another article later about the detailed implementation, but these were my main impressions after building it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FSD&lt;/code&gt; helped me clearly separate UI and model responsibilities

&lt;ul&gt;
&lt;li&gt;That made it easier to add new features without affecting existing ones too much&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;I used &lt;code&gt;is-kit&lt;/code&gt; to build user-defined type guards in a type-safe way

&lt;ul&gt;
&lt;li&gt;Adding tests made the code more robust&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Extracting magic numbers into constants made debugging much easier

&lt;ul&gt;
&lt;li&gt;Separating &lt;code&gt;config&lt;/code&gt; and &lt;code&gt;constants&lt;/code&gt; helped me tune game-related values much more comfortably&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the way, &lt;code&gt;is-kit&lt;/code&gt; is a small utility library for building type guards more easily. Feel free to check it out 😸&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;

&lt;p&gt;&lt;a href="https://dev.to/nyaomaru/building-type-guards-like-lego-blocks-making-reusable-logic-with-is-kit-48e4"&gt;https://dev.to/nyaomaru/building-type-guards-like-lego-blocks-making-reusable-logic-with-is-kit-48e4&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🤖 What it was like building a game with AI
&lt;/h2&gt;

&lt;p&gt;In my daily work I usually use &lt;strong&gt;Claude Code&lt;/strong&gt;, but for this experiment I decided to try building the game with &lt;strong&gt;&lt;code&gt;codex&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;While working on a browser game, I got the impression that this kind of UI is still pretty tricky for AI.&lt;/p&gt;
&lt;h3&gt;
  
  
  What was difficult 😅
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;When I asked it to create CSS animations, the result was often &lt;strong&gt;completely off&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;It couldn’t really think through things like &lt;strong&gt;hit detection&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Sometimes the visual motion created through DOM manipulation did not match what I actually wanted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I had to move forward while &lt;strong&gt;debugging and adjusting things manually&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In many cases, I could fix things by giving more specific instructions.&lt;/p&gt;

&lt;p&gt;But for &lt;strong&gt;CSS animation&lt;/strong&gt; and &lt;strong&gt;collision-related behavior&lt;/strong&gt;, I often had to tweak things myself while watching how the game actually moved.&lt;/p&gt;

&lt;p&gt;Because of that, my impression was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;it’s still difficult to leave UI/UX entirely to generative AI&lt;/strong&gt;, especially when the interface is unusual.&lt;/p&gt;

&lt;p&gt;Of course, for something like a standard CRUD UI, AI can already do a decent job because there are many common patterns. In real-world work, I usually don’t struggle that much. And if there’s already a mockup in Figma, the output can be quite accurate.&lt;/p&gt;

&lt;p&gt;But for &lt;strong&gt;games&lt;/strong&gt; or &lt;strong&gt;unfamiliar UI/UX&lt;/strong&gt;, human adjustment is still very necessary.&lt;/p&gt;

&lt;p&gt;That said, AI is evolving at absurd speed — faster than bamboo shoots grow.&lt;/p&gt;

&lt;p&gt;So who knows what it’ll look like soon.&lt;/p&gt;


&lt;h3&gt;
  
  
  What worked well 🙌
&lt;/h3&gt;

&lt;p&gt;Because this was a more unusual implementation, I tried to help &lt;code&gt;codex&lt;/code&gt; learn from previous mistakes ahead of time.&lt;/p&gt;

&lt;p&gt;To do that, I prepared a &lt;strong&gt;&lt;code&gt;learn&lt;/code&gt; skill&lt;/strong&gt;, along with &lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;LEARNED_INDEX.md&lt;/code&gt; so it could keep referring back to what had already gone wrong and how it had been fixed.&lt;/p&gt;

&lt;p&gt;During development, whenever an instruction didn’t work well, I extracted the lesson and saved it into files. Then I made &lt;code&gt;AGENTS.md&lt;/code&gt; load those references.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LEARNED_INDEX.md&lt;/code&gt; worked as a summary of those learned notes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;.codex/
  skills/
    learn/
      learned/
        xxx.md       &lt;span class="c"&gt;# learned notes are stored here&lt;/span&gt;
        yyy.md
        zzz.md
    LEARNED_INDEX.md &lt;span class="c"&gt;# index for referencing files under learned&lt;/span&gt;
    SKILL.md         &lt;span class="c"&gt;# a skill for summarizing what went wrong in the session&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure turned out to be really helpful for preserving instructions that normally wouldn’t appear in everyday development.&lt;/p&gt;

&lt;p&gt;For example, things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Use the boss idle bob phase relative to its spawn timing”&lt;/li&gt;
&lt;li&gt;“Make the ground charge duration explicit so the attack timing becomes stable”&lt;/li&gt;
&lt;li&gt;“Control the clear animation in phases to avoid timing conflicts”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are small details, but they matter a lot, and I wanted them to stay consistent.&lt;/p&gt;

&lt;p&gt;By repeating this process, I was able to reduce unnecessary correction instructions over time.&lt;/p&gt;

&lt;p&gt;With this kind of setup, AI becomes much easier to work with even for personal projects with very specific requirements, which felt pretty nice.&lt;/p&gt;

&lt;p&gt;Also, the &lt;code&gt;learn&lt;/code&gt; skill was inspired by the &lt;strong&gt;learn commands&lt;/strong&gt; from the &lt;code&gt;everything-claude-code&lt;/code&gt; repository. Thx! 🙏&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/affaan-m/everything-claude-code/blob/main/commands/learn.md" rel="noopener noreferrer"&gt;https://github.com/affaan-m/everything-claude-code/blob/main/commands/learn.md&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;This is a game where nyaomaru runs away from work on your behalf.&lt;/p&gt;

&lt;p&gt;What happens in the end?&lt;br&gt;
You’ll have to play it and find out.&lt;/p&gt;

&lt;p&gt;There are two endings.&lt;/p&gt;

&lt;p&gt;So go try it and see what kind of ending you get 😹&lt;/p&gt;

&lt;p&gt;Repository&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Game&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;is-kit&lt;/li&gt;
&lt;/ul&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>gamedev</category>
      <category>webdev</category>
      <category>react</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Running a SPA inside ChatGPT using MCP Apps (Step-by-Step Guide)</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Mon, 02 Feb 2026 17:32:58 +0000</pubDate>
      <link>https://dev.to/nyaomaru/running-a-spa-inside-chatgpt-using-mcp-apps-step-by-step-guide-o52</link>
      <guid>https://dev.to/nyaomaru/running-a-spa-inside-chatgpt-using-mcp-apps-step-by-step-guide-o52</guid>
      <description>&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 still uses a hot water bottle in 2026.&lt;/p&gt;

&lt;p&gt;A new era has arrived — you can now display and interact with your own web app directly inside the ChatGPT chat interface.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.modelcontextprotocol.io/posts/2026-01-26-mcp-apps/" rel="noopener noreferrer"&gt;https://blog.modelcontextprotocol.io/posts/2026-01-26-mcp-apps/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you watch the video in the announcement, you’ll see what this looks like. A new common standard called &lt;strong&gt;MCP Apps&lt;/strong&gt; has been released, allowing interactive UIs to run inside ChatGPT and Claude 🎉&lt;/p&gt;

&lt;p&gt;In simple terms:&lt;br&gt;
You can now run your own web app inside a chat, using an iframe.&lt;/p&gt;

&lt;p&gt;As a developer, that’s something I definitely wanted to try myself.&lt;/p&gt;

&lt;p&gt;In this article, I’ll walk through how to display your app inside &lt;strong&gt;ChatGPT&lt;/strong&gt; step by step.&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%2Fxi0mrnk5gjkpg9ebgv5f.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%2Fxi0mrnk5gjkpg9ebgv5f.gif" alt=" " width="760" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s dive in.&lt;/p&gt;
&lt;h2&gt;
  
  
  What are MCP Apps?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;MCP Apps provide a standardized way to deliver interactive UIs from MCP servers. Your UI renders inline in the conversation, in context, in any compliant host.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;MCP Apps are a standard that allows AI hosts to display and interact with external web UIs.&lt;/p&gt;

&lt;p&gt;Here’s the overall architecture:&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%2Fp1yh4h3sn8dsrbpt4vvr.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%2Fp1yh4h3sn8dsrbpt4vvr.png" alt=" " width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In MCP Apps, &lt;strong&gt;your UI does not talk directly to your MCP server.&lt;/strong&gt;&lt;br&gt;
Instead, &lt;strong&gt;ChatGPT (the host)&lt;/strong&gt; sits in the middle, fetching the UI and mediating all communication.&lt;/p&gt;

&lt;p&gt;Docs:&lt;br&gt;
&lt;a href="https://modelcontextprotocol.github.io/ext-apps/api/documents/Overview.html" rel="noopener noreferrer"&gt;https://modelcontextprotocol.github.io/ext-apps/api/documents/Overview.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So what does this enable?&lt;/p&gt;

&lt;p&gt;It allows ChatGPT or Claude to display a UI built with &lt;code&gt;ext-apps&lt;/code&gt;, via an MCP Server.&lt;/p&gt;

&lt;p&gt;⚠️ Important note:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Right now, the AI host must explicitly enable your app.&lt;br&gt;
This is not something users can trigger just by typing a prompt. The host needs to have your MCP server configured.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, let’s go through how to build and publish this.&lt;/p&gt;
&lt;h2&gt;
  
  
  Publishing a UI using &lt;code&gt;ext-apps&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;First, we need a web app to display.&lt;/p&gt;

&lt;p&gt;If you don’t have one, the official examples are a great starting point:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/modelcontextprotocol/ext-apps/tree/main/examples" rel="noopener noreferrer"&gt;https://github.com/modelcontextprotocol/ext-apps/tree/main/examples&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/quickstart" rel="noopener noreferrer"&gt;https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/quickstart&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I added MCP support to my experimental project nyaomaru 3D. Here’s the PR diff:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/nyaomaru-3D/pull/11/changes" rel="noopener noreferrer"&gt;https://github.com/nyaomaru/nyaomaru-3D/pull/11/changes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This works fine in a monorepo too, but here I’ll assume the frontend and MCP server live in separate repositories.&lt;/p&gt;
&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;Here’s the SPA we’ll use:&lt;/p&gt;

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

&lt;p&gt;Install &lt;code&gt;ext-apps&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@modelcontextprotocol/ext-apps" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@modelcontextprotocol/ext-apps&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @modelcontextprotocol/ext-apps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since my app uses &lt;code&gt;Vue&lt;/code&gt; + &lt;code&gt;Vite&lt;/code&gt;, I also added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add &lt;span class="nt"&gt;-D&lt;/span&gt; vite-plugin-singlefile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, create an HTML entry file for MCP (I called mine &lt;code&gt;mcp-app.html&lt;/code&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/nyaomaru-3D/blob/main/mcp-app.html" rel="noopener noreferrer"&gt;https://github.com/nyaomaru/nyaomaru-3D/blob/main/mcp-app.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then create a bundle entry:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/nyaomaru-3D/blob/main/src/mcp-app.ts" rel="noopener noreferrer"&gt;https://github.com/nyaomaru/nyaomaru-3D/blob/main/src/mcp-app.ts&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My UI component 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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;App&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="s1"&gt;@modelcontextprotocol/ext-apps&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;App&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="s1"&gt;Nyaomaru MCP App&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.1.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="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;autoResize&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;span class="nf"&gt;onMounted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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;try&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&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;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&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;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;onBeforeUnmount&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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 part is &lt;code&gt;app.connect()&lt;/code&gt; — this starts communication with the host (ChatGPT).&lt;br&gt;
If you forget this, your UI won’t work inside ChatGPT.&lt;/p&gt;
&lt;h3&gt;
  
  
  Build config
&lt;/h3&gt;

&lt;p&gt;I separated the MCP build output using a custom &lt;code&gt;Vite&lt;/code&gt; mode:&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="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;mode&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;isMcpBuild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp&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="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;vue&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;isMcpBuild&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;viteSingleFile&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;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isMcpBuild&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;outDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dist-mcp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;emptyOutDir&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="na"&gt;rollupOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fileURLToPath&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="s1"&gt;mcp-app.html&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="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a script:&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;"scripts"&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;"build:mcp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vue-tsc -b &amp;amp;&amp;amp; vite build --mode mcp"&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;h3&gt;
  
  
  Deploy
&lt;/h3&gt;

&lt;p&gt;Deploy the MCP build separately (I used &lt;code&gt;Vercel&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build Command: &lt;code&gt;pnpm build:mcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Output Directory: &lt;code&gt;dist-mcp&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;https://&lt;span class="o"&gt;{&lt;/span&gt;project-name&lt;span class="o"&gt;}&lt;/span&gt;.vercel.app/mcp-app.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this page loads, your UI is ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the MCP Server
&lt;/h2&gt;

&lt;p&gt;Now we create the MCP server that tells ChatGPT how to load our UI.&lt;/p&gt;

&lt;p&gt;Here’s my example repo:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nyaomaru/nyaomaru-3D-mcp-ui" rel="noopener noreferrer"&gt;https://github.com/nyaomaru/nyaomaru-3D-mcp-ui&lt;/a&gt;&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key parts:&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;McpServer&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="s1"&gt;@modelcontextprotocol/sdk/server/mcp.js&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;registerAppResource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;registerAppTool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;RESOURCE_MIME_TYPE&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="s1"&gt;@modelcontextprotocol/ext-apps/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;McpServer&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="s1"&gt;nyaomaru-3d-ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.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="nf"&gt;registerAppTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open-nyaomaru-3d-ui&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;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Open Nyaomaru 3D UI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Render the Nyaomaru 3D MCP UI.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;_meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;resourceUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RESOURCE_URI&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;async &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="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Opening Nyaomaru 3D UI...&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;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;registerAppResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;RESOURCE_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;RESOURCE_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RESOURCE_MIME_TYPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &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;html&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;fetchUiHtml&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;contents&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;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RESOURCE_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RESOURCE_MIME_TYPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&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;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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then expose via HTTP:&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;StreamableHTTPServerTransport&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="s1"&gt;@modelcontextprotocol/sdk/server/streamableHttp.js&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&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="nf"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/mcp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;req&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StreamableHTTPServerTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sessionIdGenerator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;enableJsonResponse&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;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&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;transport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transport&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;transport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;req&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;3001&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&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;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="s2"&gt;`MCP UI server listening on http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mcp`&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;h2&gt;
  
  
  Exposing the server with ngrok
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ngrok http 3001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the HTTPS URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring ChatGPT
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open ChatGPT&lt;/li&gt;
&lt;li&gt;Settings → Apps → Advanced → Enable Developer Mode&lt;/li&gt;
&lt;li&gt;Create a new app&lt;/li&gt;
&lt;li&gt;Enter your ngrok URL (e.g. &lt;a href="https://xxx.ngrok-free.dev/mcp" rel="noopener noreferrer"&gt;https://xxx.ngrok-free.dev/mcp&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;No auth&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then enable the app in chat and ask ChatGPT to open it.&lt;/p&gt;

&lt;p&gt;If everything worked — congrats! Your SPA is now running inside ChatGPT 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Seeing your own app running inside an AI chat interface feels like the future.&lt;/p&gt;

&lt;p&gt;We’re moving from “AI responds with text” to &lt;strong&gt;“AI hosts applications.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Right now, AI control over the iframe is limited, but this space is evolving fast.&lt;/p&gt;

&lt;p&gt;If you build products, this could soon become a new way users interact with your services.&lt;/p&gt;

&lt;p&gt;Definitely worth experimenting with now.&lt;/p&gt;

&lt;p&gt;If you’re experimenting with MCP Apps too, I’d love to see what you build 👀&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>vue</category>
      <category>chatgpt</category>
      <category>mcp</category>
    </item>
    <item>
      <title>2025: I Shipped 3 OSS Projects — “This Was Actually Fine”</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Wed, 31 Dec 2025 06:08:20 +0000</pubDate>
      <link>https://dev.to/nyaomaru/2025-i-shipped-3-oss-projects-this-was-actually-fine-3e0c</link>
      <guid>https://dev.to/nyaomaru/2025-i-shipped-3-oss-projects-this-was-actually-fine-3e0c</guid>
      <description>&lt;p&gt;Hi everyone!&lt;/p&gt;

&lt;p&gt;I’m a frontend engineer, &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;@nyaomaru&lt;/a&gt;.&lt;br&gt;&lt;br&gt;
Recently, for dieting purposes, I’ve been taking walks while carrying a backpack weighing over 10kg, kind of like Master Roshi’s (Kame-Sennin) training from Dragon Ball 🐢.&lt;/p&gt;

&lt;p&gt;Another year flew by before I noticed it.&lt;/p&gt;

&lt;p&gt;It’s already the end of the year.&lt;/p&gt;

&lt;p&gt;At the end of the year, we have cleaning, shopping, and &lt;strong&gt;reflection&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
In Japanese, December is called &lt;em&gt;“Shiwasu(師走)”&lt;/em&gt;, meaning “even teachers run because they’re so busy.”&lt;br&gt;&lt;br&gt;
As for me, I’ve been running around so much that I feel like a completely worn-out rag.&lt;/p&gt;

&lt;p&gt;Anyway, clean up this year’s mess within this year.&lt;br&gt;&lt;br&gt;
Let’s also organize our thoughts while we’re at it 🧹&lt;/p&gt;

&lt;p&gt;So, I’d like to look back on &lt;strong&gt;2025 from the perspective of OSS projects I released&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Just to be clear: &lt;strong&gt;there are no viral success stories here.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🎯 OSS Projects I Released in 2025
&lt;/h2&gt;

&lt;p&gt;This year, I released the following three OSS projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;is-kit&lt;/code&gt;&lt;/strong&gt;&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%2Fiwzzlk83ksk5q960awbd.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%2Fiwzzlk83ksk5q960awbd.png" alt=" " width="800" height="571"&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;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;changelog-bot&lt;/code&gt;&lt;/strong&gt;&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%2Fzyu78ox96p2r7j8tmtid.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%2Fzyu78ox96p2r7j8tmtid.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;divider&lt;/code&gt;&lt;/strong&gt;&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%2F9gtiy7yzg2gohhoyayuy.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%2F9gtiy7yzg2gohhoyayuy.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;I continue maintaining and releasing updates for all of them.&lt;/p&gt;


&lt;h2&gt;
  
  
  🤔 Why Did I Build Them?
&lt;/h2&gt;

&lt;p&gt;Simply put: &lt;strong&gt;because I wanted them myself&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;is-kit&lt;/code&gt;: “There must be a cleaner way to write user-defined type guards…”&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;changelog-bot&lt;/code&gt;: “Writing &lt;code&gt;CHANGELOG.md&lt;/code&gt; every time is honestly annoying…”&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;divider&lt;/code&gt;: “I just want to split strings more cleanly…”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each project started from a small frustration.&lt;br&gt;&lt;br&gt;
I asked myself, &lt;em&gt;“How can I remove this discomfort?”&lt;/em&gt; and started building.&lt;/p&gt;

&lt;p&gt;Of course, it’s great when others use your OSS.&lt;br&gt;
But my main focus was always: &lt;strong&gt;can this solve my own problem properly?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s briefly look back at each project.&lt;/p&gt;


&lt;h2&gt;
  
  
  is-kit
&lt;/h2&gt;

&lt;p&gt;If you use TypeScript, you probably write user-defined type guards fairly often.&lt;/p&gt;

&lt;p&gt;For example:&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;age&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;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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="s1"&gt;guest&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="s1"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A typical type guard might 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="kd"&gt;function&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="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;User&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="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="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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="nx"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&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="k"&gt;return &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;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I kept thinking: &lt;em&gt;“Can’t this be simpler?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That’s when the idea of &lt;strong&gt;LEGO blocks&lt;/strong&gt; came to mind.&lt;br&gt;
If we could compose small logical pieces, building complex type guards would be much easier.&lt;/p&gt;

&lt;p&gt;So I designed &lt;code&gt;is-kit&lt;/code&gt; around composable logic like &lt;code&gt;and&lt;/code&gt;, &lt;code&gt;or&lt;/code&gt;, and &lt;code&gt;not&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;is-kit&lt;/code&gt;, the same example becomes:&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;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="nx"&gt;oneOfValues&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="s1"&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="nx"&gt;struct&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;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="s1"&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="s1"&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="s1"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More declarative, more readable, and still type-safe — sounds nice, right?&lt;/p&gt;

&lt;p&gt;You can also compose guards further:&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;narrowKeyTo&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;AdminUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Readonly&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="o"&gt;&amp;amp;&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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="s1"&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;isAdminUser&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="s1"&gt;admin&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;isAdultAdmin&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;isAdminUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;predicateToRefine&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AdminUser&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;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&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;
  
  
  What Worked Well with is-kit
&lt;/h3&gt;

&lt;p&gt;A lot of people checked it out and gave it stars. Thank you so so so much 🙏🙏🙏&lt;/p&gt;

&lt;p&gt;I also discovered &lt;code&gt;tsd&lt;/code&gt;, a library for testing TypeScript type definitions, which was genuinely fun to work with.&lt;/p&gt;

&lt;p&gt;The API stayed fairly simple, and overall, I’m happy with how it turned out.&lt;br&gt;
There’s still room for improvement, so I’ll keep enhancing it.&lt;/p&gt;

&lt;p&gt;Planned improvements include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More primitive presets&lt;/li&gt;
&lt;li&gt;More combinators&lt;/li&gt;
&lt;li&gt;Improvements around &lt;code&gt;struct&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you like it, feel free to give it a ⭐️!&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;

&lt;p&gt;The goal was never magic — just composability and readability.&lt;/p&gt;


&lt;h2&gt;
  
  
  changelog-bot
&lt;/h2&gt;

&lt;p&gt;When you release an OSS project, you usually write release notes and update &lt;code&gt;CHANGELOG.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But… it’s kind of a hassle, right?&lt;/p&gt;

&lt;p&gt;At least, it was for me 🤮&lt;/p&gt;

&lt;p&gt;There are tools that generate changelogs from conventional commits, but I wondered:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Could AI classify changes based on content instead?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question led to &lt;code&gt;changelog-bot&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can use it via CLI with OpenAI or Anthropic API keys (AI is optional):&lt;/p&gt;

&lt;p&gt;It’s mainly designed to run in CI.&lt;br&gt;
Once a release is published, your &lt;code&gt;CHANGELOG.md&lt;/code&gt; can be updated automatically.&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update Changelog&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;changelog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nyaomaru/changelog-bot@v0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;changelog-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CHANGELOG.md&lt;/span&gt;
          &lt;span class="na"&gt;base-branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&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;openai&lt;/span&gt;
          &lt;span class="na"&gt;release-tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.release.tag_name }}&lt;/span&gt;
          &lt;span class="na"&gt;release-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.release.tag_name }}&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;OPENAI_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.OPENAI_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also run it locally via CLI if you want. Details are in the &lt;code&gt;README&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Worked Well with changelog-bot
&lt;/h3&gt;

&lt;p&gt;It completely removed the manual effort of maintaining &lt;code&gt;CHANGELOG.md&lt;/code&gt; for my projects.&lt;br&gt;
Releasing became much easier 🚀&lt;/p&gt;

&lt;p&gt;I personally enjoyed designing the preprocessing logic that extracts and scores features before passing data to the AI.&lt;/p&gt;

&lt;p&gt;That said, there’s still room for improvement, and contributions are welcome!&lt;/p&gt;

&lt;p&gt;One note though:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Writing changelogs became easier. &lt;strong&gt;But... life itself didn’t magically become easier&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That part is still under observation.&lt;/p&gt;

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


&lt;h2&gt;
  
  
  divider
&lt;/h2&gt;

&lt;p&gt;Sometimes you end up slicing strings over and over with &lt;code&gt;substring&lt;/code&gt;, and it gets messy.&lt;/p&gt;

&lt;p&gt;I wanted a cleaner way.&lt;/p&gt;

&lt;p&gt;So I built &lt;code&gt;divider&lt;/code&gt;, which lets you split strings in one shot using indices.&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;divider&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="s1"&gt;@nyaomaru/divider&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;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;divider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What I Learned from divider
&lt;/h3&gt;

&lt;p&gt;A great engineer contributed, I’m truly grateful 🙏&lt;/p&gt;

&lt;p&gt;More importantly, I learned the basics of OSS hygiene:&lt;br&gt;
&lt;code&gt;CODE_OF_CONDUCT.md&lt;/code&gt;, &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;, &lt;code&gt;CHANGELOG.md&lt;/code&gt;, &lt;code&gt;DEVELOPER.md&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;However, there was a clear downside.&lt;/p&gt;

&lt;p&gt;I couldn’t demonstrate a strong advantage over &lt;code&gt;string.split()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That’s reflected in the number of stars, and honestly, it was a design mistake.&lt;/p&gt;

&lt;p&gt;In short, this project was a &lt;strong&gt;“useful failure.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Still, it was a valuable learning experience, and I’m glad I built it.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  ✨ Goals for 2026
&lt;/h2&gt;

&lt;p&gt;In 2025, I:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Released OSS projects&lt;/li&gt;
&lt;li&gt;Started writing technical articles&lt;/li&gt;
&lt;li&gt;Moved to the Netherlands 🇳🇱&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It was a year full of new challenges.&lt;/p&gt;

&lt;p&gt;For 2026, I’m thinking about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Releasing OSS applications related to DSA&lt;/li&gt;
&lt;li&gt;Publishing a small game project&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But above all, my main goal is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don’t burn out. Keep going.&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;

&lt;p&gt;That applies to OSS, systems, and life itself.&lt;/p&gt;

&lt;p&gt;Thank you for reading, and I hope you have a great year ahead.&lt;/p&gt;

&lt;p&gt;See you in 2026 🐈&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>github</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Tried Reading React's Source Code and Flow Beat Me Up. So Let's Learn 🚀</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Sun, 14 Dec 2025 06:09:03 +0000</pubDate>
      <link>https://dev.to/nyaomaru/i-tried-reading-reacts-source-code-and-flow-beat-me-up-so-lets-learn-4jf0</link>
      <guid>https://dev.to/nyaomaru/i-tried-reading-reacts-source-code-and-flow-beat-me-up-so-lets-learn-4jf0</guid>
      <description>&lt;p&gt;Hi everyone!&lt;/p&gt;

&lt;p&gt;I'm &lt;strong&gt;&lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;@nyaomaru&lt;/a&gt;&lt;/strong&gt;, a frontend engineer who eats too much cheese 🧀 and is slowly gaining weight.&lt;/p&gt;

&lt;p&gt;Quick detour: this article says that to become a good engineer, you need a &lt;strong&gt;deep understanding of code&lt;/strong&gt;, and that &lt;strong&gt;reading existing code&lt;/strong&gt; is essential:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/thebitforge/10-developer-habits-that-separate-good-programmers-from-great-ones-293n"&gt;https://dev.to/thebitforge/10-developer-habits-that-separate-good-programmers-from-great-ones-293n&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I totally agree.&lt;/p&gt;

&lt;p&gt;So I started doing a &lt;strong&gt;deep reading of React's source code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And honestly?&lt;/p&gt;

&lt;p&gt;It’s been &lt;em&gt;surprisingly fun&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Every day I discover something new, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Oh, &lt;code&gt;memo&lt;/code&gt; bails out when there’s no &lt;code&gt;Fiber + props/state&lt;/code&gt; update!”&lt;/li&gt;
&lt;li&gt;“Wait, &lt;code&gt;&amp;lt;Activity&amp;gt;&lt;/code&gt; in &lt;code&gt;v19.2&lt;/code&gt; is wrapped with &lt;code&gt;Offscreen&lt;/code&gt; and switches behavior by mode?”&lt;/li&gt;
&lt;li&gt;“They use &lt;strong&gt;bit flags&lt;/strong&gt; instead of booleans… no wonder it’s fast even with complex state!”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have some free time during the holidays, I highly recommend trying it.&lt;br&gt;
How about &lt;strong&gt;“React deep reading without giving up”&lt;/strong&gt; for New Year’s Eve?&lt;br&gt;&lt;br&gt;
…just a thought.&lt;/p&gt;


&lt;h2&gt;
  
  
  But then… Flow happened 😇
&lt;/h2&gt;

&lt;p&gt;As I kept reading, I hit a wall.&lt;/p&gt;

&lt;p&gt;I’m used to reading &lt;strong&gt;TypeScript&lt;/strong&gt;, but React’s core is written in &lt;strong&gt;Flow&lt;/strong&gt;, and there are many places where Flow’s type system behaves differently.&lt;/p&gt;

&lt;p&gt;Honestly, I almost gave up.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Wait… is this syntax? a type? a comment?&lt;br&gt;
&lt;strong&gt;…a spell?&lt;/strong&gt;”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My brain felt like it was being beaten up &lt;em&gt;physically&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So I decided to write this article with one goal:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Understand Flow’s quirks quickly, and lower the barrier to reading React’s source code.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;People who want to read React’s source code more deeply&lt;/li&gt;
&lt;li&gt;People who’ve heard of Flow, but never really understood it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alright, let’s dive in!&lt;/p&gt;


&lt;h2&gt;
  
  
  🌊 What is Flow?
&lt;/h2&gt;

&lt;p&gt;First, let’s clarify what Flow actually is.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://flow.org/" rel="noopener noreferrer"&gt;https://flow.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flow is a static type checker for JavaScript.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At first glance it feels similar to TypeScript, but the philosophy is different:&lt;br&gt;
Flow is much closer to a &lt;strong&gt;pure type system&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Flow has been developed by &lt;strong&gt;Meta (formerly Facebook)&lt;/strong&gt; alongside React itself, so React’s core type definitions are deeply written in Flow.&lt;/p&gt;

&lt;p&gt;The key point is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Flow adds type safety &lt;em&gt;on top of&lt;/em&gt; JavaScript.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
It does &lt;em&gt;not&lt;/em&gt; extend JavaScript with new language features like TypeScript does.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That difference matters a lot when you read React’s internals.&lt;/p&gt;


&lt;h2&gt;
  
  
  🧩 Why should we learn Flow?
&lt;/h2&gt;

&lt;p&gt;If you read React’s source code, you’ll constantly encounter Flow types in core concepts like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Suspense&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Fiber&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Lanes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Offscreen&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Especially tricky concepts include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;variance&lt;/code&gt; (covariant / contravariant / invariant)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maybe&lt;/code&gt; (&lt;code&gt;?T&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mixed&lt;/code&gt; / &lt;code&gt;empty&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exact objects&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many of these &lt;strong&gt;aren't the same meanings in TypeScript&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Without Flow knowledge, you’ll often stop and wonder:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Is this a type? syntax? or some utility?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That friction adds up and makes reading slower — and easier to give up.&lt;/p&gt;

&lt;p&gt;Once you understand Flow, &lt;strong&gt;React’s source code suddenly becomes much more readable&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  ⚔️ Flow vs TypeScript (quick comparison)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Flow&lt;/th&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary goal&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Static type checking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Static types + language extensions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strictness&lt;/td&gt;
&lt;td&gt;Stricter / theory-oriented&lt;/td&gt;
&lt;td&gt;Practical / sometimes permissive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notable features&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Exact Object&lt;/code&gt;, &lt;code&gt;variance&lt;/code&gt;, &lt;code&gt;opaque types&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Rich unions, enums&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React core&lt;/td&gt;
&lt;td&gt;Written in Flow&lt;/td&gt;
&lt;td&gt;Apps mostly written in TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning benefit&lt;/td&gt;
&lt;td&gt;Understand React internals&lt;/td&gt;
&lt;td&gt;Best for app development&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;👉 &lt;strong&gt;Flow is NOT “the origin of TypeScript.”&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
They evolved on &lt;strong&gt;different paths&lt;/strong&gt;, with different goals.&lt;/p&gt;


&lt;h2&gt;
  
  
  🌀 Flow Types We’ll Focus On
&lt;/h2&gt;

&lt;p&gt;We’ll skip the basics that feel similar to TypeScript, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Primitive types&lt;/li&gt;
&lt;li&gt;Literal types&lt;/li&gt;
&lt;li&gt;Functions&lt;/li&gt;
&lt;li&gt;Utilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, we’ll focus on Flow types that often confuse people when reading React:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mixed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;empty&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maybe&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;opaque&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exact&lt;/code&gt; / &lt;code&gt;inexact objects&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(There are many more — check the &lt;a href="https://flow.org/" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; if you’re curious!)&lt;/p&gt;

&lt;p&gt;For variance, see this article:&lt;br&gt;
&lt;a href="https://dev.to/nyaomaru/understanding-variance-in-typescript-flow-covariant-contravariant-invariant-bivariant-4fbi"&gt;https://dev.to/nyaomaru/understanding-variance-in-typescript-flow-covariant-contravariant-invariant-bivariant-4fbi&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🌪️ Understanding &lt;code&gt;mixed&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Let’s start with &lt;code&gt;mixed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In Flow, &lt;code&gt;mixed&lt;/code&gt; is very similar to TypeScript’s &lt;code&gt;unknown&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You’ll see it in React when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;handling errors&lt;/li&gt;
&lt;li&gt;dealing with callbacks&lt;/li&gt;
&lt;li&gt;accepting “anything, but safely”&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;mixed&lt;/code&gt; is the &lt;em&gt;safest&lt;/em&gt; “anything”
&lt;/h3&gt;

&lt;p&gt;From a type theory perspective, &lt;code&gt;mixed&lt;/code&gt; is a &lt;strong&gt;top type&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Any value can be here, but you’re not allowed to use it until you prove what it is.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unlike TypeScript’s &lt;code&gt;any&lt;/code&gt;, which says “do whatever you want,”&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mixed&lt;/code&gt; says: &lt;strong&gt;“Show me the type first.”&lt;/strong&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;function&lt;/span&gt; &lt;span class="nf"&gt;numberOrString&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="nx"&gt;mixed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// x + 1;   // ❌ error&lt;/span&gt;
  &lt;span class="c1"&gt;// x.name; // ❌ error&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;x&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;x&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="c1"&gt;// ✅ OK&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 &lt;strong&gt;must narrow&lt;/strong&gt; before using it.&lt;/p&gt;

&lt;p&gt;This prevents bugs caused by casually handling unknown values.&lt;/p&gt;

&lt;p&gt;If you know &lt;code&gt;unknown&lt;/code&gt; in TypeScript, this should feel very familiar.&lt;/p&gt;




&lt;h2&gt;
  
  
  🕳️ Understanding empty
&lt;/h2&gt;

&lt;p&gt;Next up: &lt;code&gt;empty&lt;/code&gt;.&lt;br&gt;
(Don’t worry — not talking about &lt;strong&gt;my bank account&lt;/strong&gt;.)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;empty&lt;/code&gt; represents the &lt;strong&gt;bottom type&lt;/strong&gt; in Flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;empty&lt;/code&gt; means “this value can never exist”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Literally:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“There is no possible value of this type.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not &lt;code&gt;number&lt;/code&gt;, not &lt;code&gt;string&lt;/code&gt;, not &lt;code&gt;null&lt;/code&gt; — nothing.&lt;/p&gt;

&lt;p&gt;This is very close to TypeScript’s &lt;code&gt;never&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;function&lt;/span&gt; &lt;span class="nf"&gt;foo&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="nx"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;empty&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;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// type-checks, but can never be called&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function is &lt;em&gt;logically&lt;/em&gt; unreachable.&lt;/p&gt;

&lt;p&gt;Flow often uses &lt;code&gt;empty&lt;/code&gt; to represent &lt;strong&gt;impossible states.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is this useful?
&lt;/h3&gt;

&lt;p&gt;Example: unreachable branches.&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;function&lt;/span&gt; &lt;span class="nf"&gt;bar&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="nx"&gt;string&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ❌ unreachable → inferred as empty&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;Or functions that never return:&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;function&lt;/span&gt; &lt;span class="nf"&gt;throwError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;empty&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="nx"&gt;message&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;Or &lt;strong&gt;exhaustive checks&lt;/strong&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;function&lt;/span&gt; &lt;span class="nf"&gt;voice&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cat&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="s1"&gt;dog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &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="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cat&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;meow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dog&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;default&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="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;empty&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 forget a case, Flow will complain — at compile time.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌫️ Understanding &lt;code&gt;maybe&lt;/code&gt; (&lt;code&gt;?T&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Flow’s &lt;code&gt;maybe&lt;/code&gt; type is written as &lt;code&gt;?T&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It means:&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;T&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;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much shorter, right?&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;function&lt;/span&gt; &lt;span class="nf"&gt;greet&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;span class="nx"&gt;string&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;name&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;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;name&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="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello!&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;Using &lt;code&gt;!= null&lt;/code&gt; is intentional here — it safely removes both &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🕶️ Understanding &lt;code&gt;opaque&lt;/code&gt; types
&lt;/h2&gt;

&lt;p&gt;Now for one of Flow’s most stylish features: opaque types.&lt;/p&gt;

&lt;p&gt;Normally, if two types share the same structure, they’re interchangeable.&lt;/p&gt;

&lt;p&gt;Flow lets you say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“They look the same — but they are NOT the same.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  TypeScript alias example
&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;UserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;string&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;PostId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;string&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="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aaa&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;post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PostId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// allowed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aliases are just aliases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flow opaque types
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;opaque&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;opaque&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PostId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Locally they look similar — but export them, and things change.&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;export&lt;/span&gt; &lt;span class="nx"&gt;opaque&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Outside the module, &lt;code&gt;UserId&lt;/code&gt; becomes &lt;strong&gt;truly opaque&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You cannot forge it.&lt;/p&gt;

&lt;p&gt;This gives Flow &lt;strong&gt;true nominal typing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;TypeScript’s branded types are similar — but can be bypassed with casts.&lt;br&gt;
Flow’s opaque types &lt;strong&gt;cannot be forged outside the module&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That’s why they’re so powerful.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧩 Exact vs Inexact Objects
&lt;/h2&gt;

&lt;p&gt;This is not a personality test.&lt;/p&gt;

&lt;p&gt;Flow objects come in two flavors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exact (default)&lt;/strong&gt; — strict&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inexact (&lt;code&gt;...&lt;/code&gt;)&lt;/strong&gt; — permissive&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Inexact
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&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;user&lt;/span&gt;&lt;span class="p"&gt;:&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nyaomaru&lt;/span&gt;&lt;span class="dl"&gt;'&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="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// OK&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Extra properties are allowed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exact (default)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="p"&gt;:&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nyaomaru&lt;/span&gt;&lt;span class="dl"&gt;'&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="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// ❌ error&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even passing through variables won’t bypass this.&lt;/p&gt;

&lt;p&gt;This strictness prevents accidental object pollution — something TypeScript allows in some cases.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏁 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Once you understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mixed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;empty&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maybe&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;opaque&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exact&lt;/code&gt; / &lt;code&gt;inexact&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;React’s source code stops looking like magic spells.&lt;/p&gt;

&lt;p&gt;You start thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Ah — that’s why they typed it this way.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So grab some noodles (or pasta 🍝), and try reading React’s source code again.&lt;/p&gt;

&lt;p&gt;Happy holidays, and happy hacking!&lt;/p&gt;




&lt;h2&gt;
  
  
  Bonus
&lt;/h2&gt;

&lt;p&gt;You can try to use flow in below playground 🚀&lt;/p&gt;

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

&lt;p&gt;Please check my OSS for building type guards easily: &lt;code&gt;is-kit&lt;/code&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%2Fzf2cy0u8e5d5y8r1a216.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%2Fzf2cy0u8e5d5y8r1a216.png" alt=" " width="634" height="175"&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>javascript</category>
      <category>react</category>
      <category>typescript</category>
      <category>programming</category>
    </item>
    <item>
      <title>Generate CHANGELOG.md Automatically 🤖</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Sun, 30 Nov 2025 13:15:01 +0000</pubDate>
      <link>https://dev.to/nyaomaru/generate-changelogmd-automatically-26g1</link>
      <guid>https://dev.to/nyaomaru/generate-changelogmd-automatically-26g1</guid>
      <description>&lt;p&gt;Hey everyone! Happy winter is upcoming! ⛄️&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.&lt;/p&gt;

&lt;p&gt;Today I’d like to introduce &lt;strong&gt;&lt;code&gt;changelog-bot&lt;/code&gt;&lt;/strong&gt;, a tool that automatically generates a polished &lt;code&gt;CHANGELOG.md&lt;/code&gt; from your release notes! 🚀&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Background
&lt;/h2&gt;

&lt;p&gt;If you’re maintaining a project, you’ve probably used &lt;code&gt;CHANGELOG.md&lt;/code&gt; to keep track of release details.&lt;br&gt;
But let’s be honest—updating it manually with every version bump is &lt;em&gt;tedious&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Sure, you can automate parts of it, but most generators produce unstructured or messy output.&lt;br&gt;
That’s exactly the pain point that inspired &lt;strong&gt;&lt;code&gt;changelog-bot&lt;/code&gt;&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%2Fudphqqcsl63ac3cid0yh.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%2Fudphqqcsl63ac3cid0yh.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

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


&lt;h2&gt;
  
  
  🎁 Why changelog-bot?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🖋 &lt;strong&gt;Automates&lt;/strong&gt; the boring part — generating &lt;code&gt;CHANGELOG.md&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Uses AI to structure and format the changelog with high accuracy&lt;/li&gt;
&lt;li&gt;Analyzes commit messages and PR titles to automatically categorize “fix”, “feat”, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works directly from release notes&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;No need for strict Conventional Commit rules&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works even without AI&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Fallback logic builds changelogs accurately even without an API key&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  🔎 How to Use It
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Try it instantly (npx / pnpx)
&lt;/h3&gt;

&lt;p&gt;The easiest way to try it out is from the CLI using &lt;code&gt;npx&lt;/code&gt; or &lt;code&gt;pnpm dlx&lt;/code&gt;!&lt;/p&gt;
&lt;h4&gt;
  
  
  pnpm
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm dlx @nyaomaru/changelog-bot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--release-tag&lt;/span&gt; v0.0.1 &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;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;💡 Tip: Remove --dry-run to actually create a PR with your updated CHANGELOG.md!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want to enable AI-assisted formatting, set your API key beforehand:&lt;/p&gt;

&lt;p&gt;For security reasons, &lt;code&gt;.env&lt;/code&gt; files are not automatically loaded.&lt;br&gt;
Please export your environment variables manually.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# For OpenAI users&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-xxxx   &lt;span class="c"&gt;# Use OpenAI as the provider&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gpt-4o-mini &lt;span class="c"&gt;# Optional: specify a model&lt;/span&gt;

&lt;span class="c"&gt;# For Anthropic users&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-ant-xxxx
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;claude-3-5-sonnet-20240620
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Setting up a local dev environment with mise
&lt;/h3&gt;

&lt;p&gt;If you’d like to explore the source or contribute, set up Node and pnpm with mise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mise &lt;span class="nb"&gt;install&lt;/span&gt;     &lt;span class="c"&gt;# Installs Node 22 / pnpm 10.12&lt;/span&gt;
mise dev_install &lt;span class="c"&gt;# Installs dependencies&lt;/span&gt;
mise build       &lt;span class="c"&gt;# Compiles TypeScript&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, export your environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;REPO_FULL_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_name/your_repository_name &lt;span class="c"&gt;# Target repository&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ghp_xxx                          &lt;span class="c"&gt;# GitHub token&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-xxx                         &lt;span class="c"&gt;# Optional: for AI formatting&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, just run the CLI — make sure to set your version properly!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mise start &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;--release-tag&lt;/span&gt; v0.0.1 &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;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 Tip: Remove --dry-run to apply changes to your CHANGELOG.md via PR!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;If no API key is provided, it falls back to commit log analysis&lt;/li&gt;
&lt;li&gt;You can swap models by setting &lt;code&gt;OPENAI_MODEL&lt;/code&gt; or &lt;code&gt;ANTHROPIC_MODEL&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Automate with GitHub Actions
&lt;/h3&gt;

&lt;p&gt;Even easier—let GitHub Actions handle it.&lt;br&gt;
Just drop a workflow like this under &lt;code&gt;.github/workflows/&lt;/code&gt; and your changelog will grow automatically whenever a release tag is pushed:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Auto Changelog&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v*'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;changelog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nyaomaru/changelog-bot/.github/workflows/changelog.yaml@v0&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;changelog_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CHANGELOG.md&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;openai&lt;/span&gt;
      &lt;span class="na"&gt;release_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.ref_name }}&lt;/span&gt;
      &lt;span class="na"&gt;dry_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;
    &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;REPO_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
      &lt;span class="na"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.OPENAI_API_KEY }}&lt;/span&gt;
      &lt;span class="na"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ANTHROPIC_API_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don’t want to expose AI keys, you can set &lt;code&gt;dry_run: 'true'&lt;/code&gt; to post a draft changelog as a PR comment instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌱 Distribution &amp;amp; Ecosystem
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Available as an npm/pnpm CLI package (&lt;code&gt;@nyaomaru/changelog-bot&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Also usable as a GitHub Action via the included &lt;code&gt;action.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The fallback logic doesn’t require Conventional Commits — easy to drop into any repo&lt;/li&gt;
&lt;li&gt;Issues and PRs are always welcome! Templates are ready, so feel free to leave feedback 😹&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Future Plans
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Support for local LLMs as changelog classifiers&lt;/li&gt;
&lt;li&gt;Multi-language changelog output&lt;/li&gt;
&lt;li&gt;Improved accuracy via preprocessing and better prompts&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;&lt;code&gt;changelog-bot&lt;/code&gt; is both a &lt;strong&gt;CLI&lt;/strong&gt; and a &lt;strong&gt;GitHub Action&lt;/strong&gt; that automatically generates &lt;code&gt;CHANGELOG.md&lt;/code&gt; whenever a release is triggered.&lt;/p&gt;

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

&lt;p&gt;If you like it, don’t forget to leave a 🌟 — it keeps me going! 😻&lt;/p&gt;




&lt;h2&gt;
  
  
  Bonus
&lt;/h2&gt;

&lt;p&gt;I’ve also released another OSS project called &lt;strong&gt;is-kit&lt;/strong&gt; — a utility for building powerful &lt;code&gt;isXXX&lt;/code&gt; type guards in TypeScript.&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%2F3ro5orqoc2fcp4osfa1z.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%2F3ro5orqoc2fcp4osfa1z.png" alt=" " width="800" height="571"&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;

&lt;p&gt;Check out the related articles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/nyaomaru/escaping-the-forest-of-if-statements-building-logical-type-guards-with-is-kit-2db3"&gt;https://dev.to/nyaomaru/escaping-the-forest-of-if-statements-building-logical-type-guards-with-is-kit-2db3&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/nyaomaru/building-type-guards-like-lego-blocks-making-reusable-logic-with-is-kit-48e4"&gt;https://dev.to/nyaomaru/building-type-guards-like-lego-blocks-making-reusable-logic-with-is-kit-48e4&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/nyaomaru/build-isxxx-the-easy-way-meet-is-kit-2hpl"&gt;https://dev.to/nyaomaru/build-isxxx-the-easy-way-meet-is-kit-2hpl&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>ci</category>
      <category>opensource</category>
    </item>
    <item>
      <title>🧠 Understanding Variance in TypeScript &amp; Flow: Covariant, Contravariant, Invariant, Bivariant</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Wed, 26 Nov 2025 05:46:26 +0000</pubDate>
      <link>https://dev.to/nyaomaru/understanding-variance-in-typescript-flow-covariant-contravariant-invariant-bivariant-4fbi</link>
      <guid>https://dev.to/nyaomaru/understanding-variance-in-typescript-flow-covariant-contravariant-invariant-bivariant-4fbi</guid>
      <description>&lt;p&gt;Hi everyone!&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 quietly moved to the Netherlands. 🇳🇱&lt;/p&gt;

&lt;p&gt;If you write &lt;code&gt;TypeScript&lt;/code&gt;, you’ve probably bumped into the term &lt;strong&gt;“variance”&lt;/strong&gt; at some point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;covariant&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;contravariant&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;invariant&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;bivariant&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You may have a vague feeling of “I sorta get it… but not really.”&lt;/p&gt;

&lt;p&gt;Personally, I struggled especially with &lt;strong&gt;contravariance&lt;/strong&gt; and &lt;strong&gt;bivariance&lt;/strong&gt; — they’re really counter-intuitive.&lt;/p&gt;

&lt;p&gt;And when I tried to deep-read &lt;code&gt;React&lt;/code&gt;’s type definitions, I ran into &lt;code&gt;Flow&lt;/code&gt;’s variance annotations &lt;code&gt;+T&lt;/code&gt; / &lt;code&gt;-T&lt;/code&gt; and completely froze:&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;export&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Element&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;+&lt;/span&gt;&lt;span class="nx"&gt;C&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;React$Element&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;C&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;export&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RefSetter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;I&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;React$RefSetter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;I&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;“What are &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;-&lt;/code&gt;!?”&lt;br&gt;
“Why is &lt;code&gt;React&lt;/code&gt; using this in &lt;code&gt;Flow&lt;/code&gt;!?”&lt;/p&gt;

&lt;p&gt;That was the &lt;strong&gt;entrance&lt;/strong&gt; to understand variance for me.&lt;/p&gt;

&lt;p&gt;In this article, I’ll use both &lt;code&gt;TypeScript&lt;/code&gt; and &lt;code&gt;Flow&lt;/code&gt; to build a &lt;strong&gt;practical&lt;/strong&gt;, &lt;strong&gt;real-world&lt;/strong&gt; understanding of variance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want to read &lt;code&gt;React’s&lt;/code&gt; type definitions without crying 😿&lt;/li&gt;
&lt;li&gt;You want to design safe callback types in &lt;code&gt;TypeScript&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You want to avoid the bivariant foot-guns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s dive in 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;br&gt;
I won’t do a full &lt;code&gt;Flow&lt;/code&gt; tutorial here. I’ll only touch &lt;code&gt;Flow&lt;/code&gt; enough to explain how it expresses variance.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  🧩 What is type variance?
&lt;/h2&gt;

&lt;p&gt;“Type variance” is about generic types and function types, and:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;how the subtype relationship between type parameters propagates to the outer type&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, suppose &lt;code&gt;Cat&lt;/code&gt; is a subtype of &lt;code&gt;Animal&lt;/code&gt; (&lt;code&gt;Cat &amp;lt;: Animal&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Then is &lt;code&gt;List&amp;lt;Cat&amp;gt;&lt;/code&gt; also a subtype of &lt;code&gt;List&amp;lt;Animal&amp;gt;&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Can we use &lt;code&gt;Handler&amp;lt;Animal&amp;gt;&lt;/code&gt; where a &lt;code&gt;Handler&amp;lt;Cat&amp;gt;&lt;/code&gt; is expected?&lt;/li&gt;
&lt;li&gt;What about something mutable like &lt;code&gt;Box&amp;lt;T&amp;gt;&lt;/code&gt;?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Variance is the set of rules that determines these “generic subtype” relationships.&lt;/p&gt;

&lt;p&gt;There are four basic flavors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Covariant&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Subtype relationship goes in the &lt;strong&gt;same direction&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contravariant&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Subtype relationship goes in the &lt;strong&gt;opposite direction&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invariant&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;No subtype relationship either way&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bivariant&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Both directions are allowed (&lt;code&gt;TypeScript&lt;/code&gt; foot-gun)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not specific to &lt;code&gt;TypeScript&lt;/code&gt; — you’ll find them in &lt;code&gt;Java&lt;/code&gt;, &lt;code&gt;C#&lt;/code&gt;, &lt;code&gt;Kotlin&lt;/code&gt;, &lt;code&gt;Flow&lt;/code&gt;, and pretty much any typed language that has generics.&lt;/p&gt;

&lt;p&gt;Let’s go through them one by one.&lt;/p&gt;
&lt;h3&gt;
  
  
  ⬆️ Covariant: “If child is OK, using it as parent is also OK”
&lt;/h3&gt;

&lt;p&gt;Given &lt;code&gt;Cat &amp;lt;: Animal&lt;/code&gt;, covariance is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Covariant: &lt;code&gt;F&amp;lt;Cat&amp;gt; &amp;lt;: F&amp;lt;Animal&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Roughly: a “container of more specific things” can be used where a “container of more general things” is expected.&lt;/p&gt;

&lt;p&gt;This is typically used for &lt;strong&gt;read-only&lt;/strong&gt; types.&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;class&lt;/span&gt; &lt;span class="nc"&gt;Animal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;animal&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;class&lt;/span&gt; &lt;span class="nc"&gt;Cat&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Animal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;meow&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;ReadonlyBox&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="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;value&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;catBox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReadonlyBox&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Cat&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="na"&gt;value&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;Cat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Using "box of Cat" as "box of Animal" is fine&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animalBox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReadonlyBox&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Animal&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;catBox&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Reading as Animal is always safe&lt;/span&gt;
&lt;span class="c1"&gt;// animalBox.value.meow(); // Type error: Animal doesn't have meow()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we only &lt;strong&gt;read&lt;/strong&gt; from the box, so it’s safe to treat a &lt;code&gt;ReadonlyBox&amp;lt;Cat&amp;gt;&lt;/code&gt; as a &lt;code&gt;ReadonlyBox&amp;lt;Animal&amp;gt;&lt;/code&gt;. That’s &lt;strong&gt;covariance&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%2Fccipn9wpbmtyblauouf6.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%2Fccipn9wpbmtyblauouf6.png" alt=" " width="800" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top: &lt;code&gt;Cat&lt;/code&gt; → &lt;code&gt;Animal&lt;/code&gt; (usual subtype relation)&lt;/li&gt;
&lt;li&gt;Bottom: &lt;code&gt;ReadonlyBox&amp;lt;Cat&amp;gt;&lt;/code&gt; → &lt;code&gt;ReadonlyBox&amp;lt;Animal&amp;gt;&lt;/code&gt; (same direction → covariant)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⬇️ Contravariant: “If parent is OK, you can use it in a child-only slot”
&lt;/h3&gt;

&lt;p&gt;Again with &lt;code&gt;Cat &amp;lt;: Animal&lt;/code&gt;, contravariance is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Contravariant: &lt;code&gt;F&amp;lt;Animal&amp;gt;&lt;/code&gt; &amp;lt;: &lt;code&gt;F&amp;lt;Cat&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The idea is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A function that can handle a wider type can safely be used wherever a narrower type handler is required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This comes up with &lt;strong&gt;function parameter types&lt;/strong&gt; (i.e., “write-only” positions).&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;class&lt;/span&gt; &lt;span class="nc"&gt;Animal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;animal&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;class&lt;/span&gt; &lt;span class="nc"&gt;Cat&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Animal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;meow&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;Handler&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="nx"&gt;value&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&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;handleAnimal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Handler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Animal&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="nx"&gt;animal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Animal&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;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="nx"&gt;animal&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleCat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Handler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Cat&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="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cat&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;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;meow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Safe with contravariance:&lt;/span&gt;
&lt;span class="c1"&gt;// A handler that accepts any Animal can be used as a Cat-specific handler&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;catHandler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Handler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Cat&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;handleAnimal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// OK (in theory)&lt;/span&gt;

&lt;span class="c1"&gt;// The opposite is unsafe: a Cat-only handler&lt;/span&gt;
&lt;span class="c1"&gt;// cannot safely handle all Animals (Dog, Bird, ...)&lt;/span&gt;
&lt;span class="c1"&gt;// const animalHandler: Handler&amp;lt;Animal&amp;gt; = handleCat; // Should be an error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Handler&amp;lt;Animal&amp;gt;&lt;/code&gt; can handle any animal (including &lt;code&gt;Cat&lt;/code&gt;), so it’s safe to use where a “Cat handler” is expected.&lt;/p&gt;

&lt;p&gt;The reverse is &lt;strong&gt;not&lt;/strong&gt; safe → that’s contravariance.&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%2F9knw3k5y0a4tcht7qogr.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%2F9knw3k5y0a4tcht7qogr.png" alt=" " width="800" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top: &lt;code&gt;Cat&lt;/code&gt; → &lt;code&gt;Animal&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Bottom: &lt;code&gt;Handler&amp;lt;Animal&amp;gt;&lt;/code&gt; → &lt;code&gt;Handler&amp;lt;Cat&amp;gt;&lt;/code&gt; (reverse direction → contravariant)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⛔ Invariant: “No subtyping either way”
&lt;/h3&gt;

&lt;p&gt;Even if &lt;code&gt;Cat &amp;lt;: Animal&lt;/code&gt; , with invariance:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Invariant: &lt;code&gt;F&amp;lt;Cat&amp;gt;&lt;/code&gt; and &lt;code&gt;F&amp;lt;Animal&amp;gt;&lt;/code&gt; have &lt;strong&gt;no&lt;/strong&gt; subtype relation&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Animal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;animal&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;class&lt;/span&gt; &lt;span class="nc"&gt;Cat&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Animal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;meow&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;Box&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="na"&gt;value&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;animalBox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Animal&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="na"&gt;value&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;Animal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;catBox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Cat&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="na"&gt;value&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;Cat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Both of these are theoretically unsafe:&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// animalBox = catBox;&lt;/span&gt;
&lt;span class="c1"&gt;// catBox = animalBox;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you treat &lt;code&gt;Box&amp;lt;Cat&amp;gt;&lt;/code&gt; as &lt;code&gt;Box&amp;lt;Animal&amp;gt;&lt;/code&gt;:

&lt;ul&gt;
&lt;li&gt;You could write a plain &lt;code&gt;Animal&lt;/code&gt; into it&lt;/li&gt;
&lt;li&gt;But someone else might assume it still only contains &lt;code&gt;Cat&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;If you treat &lt;code&gt;Box&amp;lt;Animal&amp;gt;&lt;/code&gt; as &lt;code&gt;Box&amp;lt;Cat&amp;gt;&lt;/code&gt;:

&lt;ul&gt;
&lt;li&gt;You might pull an &lt;code&gt;Animal&lt;/code&gt; from it and assume it’s always a &lt;code&gt;Cat&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Since it’s &lt;strong&gt;mutable&lt;/strong&gt; and used for both read and write, the safe option is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make it invariant (no subtyping).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In pure type theory we’d reject both assignments.&lt;/p&gt;

&lt;p&gt;In practice, TypeScript treats most generics as approximately covariant, so &lt;code&gt;Box&amp;lt;Cat&amp;gt;&lt;/code&gt; → &lt;code&gt;Box&amp;lt;Animal&amp;gt;&lt;/code&gt; may compile — but conceptually, &lt;code&gt;Box&amp;lt;T&amp;gt;&lt;/code&gt; should be invariant.&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%2Ft0ygcdcqyapn4zci25ro.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%2Ft0ygcdcqyapn4zci25ro.png" alt=" " width="800" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top: &lt;code&gt;Cat&lt;/code&gt; → &lt;code&gt;Animal&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Bottom: no arrow between &lt;code&gt;Box&amp;lt;Cat&amp;gt;&lt;/code&gt; and &lt;code&gt;Box&amp;lt;Animal&amp;gt;&lt;/code&gt; → invariant&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔁 Bivariant: “Both directions are OK (and that’s exactly the problem)” — TypeScript’s hole
&lt;/h3&gt;

&lt;p&gt;Given &lt;code&gt;Cat &amp;lt;: Animal&lt;/code&gt;, bivariance is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Bivariant: &lt;code&gt;F&amp;lt;Cat&amp;gt;&lt;/code&gt; and &lt;code&gt;F&amp;lt;Animal&amp;gt;&lt;/code&gt; are mutually assignable&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So it’s like “covariant + contravariant at the same time”.&lt;br&gt;
From a soundness standpoint, this is pretty much always a bad idea.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;TypeScript&lt;/code&gt; allows it in some cases (esp. some callback parameter types) for &lt;code&gt;JavaScript&lt;/code&gt; compatibility.&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;EventHandler&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="nx"&gt;event&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&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;let&lt;/span&gt; &lt;span class="nx"&gt;handleEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EventHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Event&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;declare&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;handleMouse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EventHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MouseEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// In sound type theory, only one of these would be allowed (depending on design).&lt;/span&gt;
&lt;span class="c1"&gt;// In TypeScript, *both* can be allowed in certain contexts (bivariant).&lt;/span&gt;
&lt;span class="nx"&gt;handleEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;handleMouse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// OK in some cases (but can be unsafe)&lt;/span&gt;
&lt;span class="nx"&gt;handleMouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;handleEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Also OK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;MouseEvent&lt;/code&gt; is a subtype of &lt;code&gt;Event&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If both directions are allowed, you can pass the “wrong” handler&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TypeScript&lt;/code&gt; chooses practicality over strict safety here&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%2F6f7xvofg602t5pi5539e.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%2F6f7xvofg602t5pi5539e.png" alt=" " width="800" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top: &lt;code&gt;MouseEvent&lt;/code&gt; → &lt;code&gt;Event&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Bottom: &lt;code&gt;Handler&amp;lt;MouseEvent&amp;gt;&lt;/code&gt; ⇔ &lt;code&gt;Handler&amp;lt;Event&amp;gt;&lt;/code&gt; → bivariant&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🎯 Quick summary of the four
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind&lt;/th&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Safety&lt;/th&gt;
&lt;th&gt;Typical example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Covariant&lt;/td&gt;
&lt;td&gt;Child → Parent&lt;/td&gt;
&lt;td&gt;Safe for reads&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;readonly T[]&lt;/code&gt;, &lt;code&gt;ReadonlyArray&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contravariant&lt;/td&gt;
&lt;td&gt;Parent → Child&lt;/td&gt;
&lt;td&gt;Safe for writes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;(arg: T) =&amp;gt; void&lt;/code&gt;, handlers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invariant&lt;/td&gt;
&lt;td&gt;No subtyping&lt;/td&gt;
&lt;td&gt;Safe for mutable&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Box&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bivariant&lt;/td&gt;
&lt;td&gt;Both directions&lt;/td&gt;
&lt;td&gt;Unsound / risky&lt;/td&gt;
&lt;td&gt;Some TS callback parameter positions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  ⚙️ &lt;code&gt;TypeScript&lt;/code&gt;’s reality vs. “pure” type theory
&lt;/h2&gt;

&lt;p&gt;So far we’ve been in the “beautiful, clean theory world”:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read-only → &lt;strong&gt;covariant&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Write-only (function parameters) → &lt;strong&gt;contravariant&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Mutable read + write → &lt;strong&gt;invariant&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Both directions → &lt;strong&gt;unsound (bivariant)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But &lt;code&gt;TypeScript&lt;/code&gt; does not fully implement this ideal.&lt;/p&gt;

&lt;p&gt;Because of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Historical &lt;code&gt;JavaScript&lt;/code&gt; APIs&lt;/li&gt;
&lt;li&gt;Massive existing &lt;code&gt;JavaScript&lt;/code&gt; codebases&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Browser&lt;/code&gt; / &lt;code&gt;DOM&lt;/code&gt; APIs design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;TypeScript&lt;/code&gt; makes several pragmatic compromises:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;T[]&lt;/code&gt; arrays are &lt;strong&gt;treated as covariant&lt;/strong&gt;, even though they should be invariant&lt;/li&gt;
&lt;li&gt;Function parameter positions are &lt;strong&gt;often bivariant&lt;/strong&gt;, not strict contravariant&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mismatch between &lt;strong&gt;type theory intuition&lt;/strong&gt; and &lt;strong&gt;TS behavior&lt;/strong&gt; is where a lot of confusion comes from.&lt;/p&gt;

&lt;p&gt;Let’s look at the two big ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Arrays: should be invariant, but TS treats them as “basically covariant”
&lt;/h3&gt;

&lt;p&gt;Formally:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;T[]&lt;/code&gt; should really be &lt;strong&gt;invariant&lt;/strong&gt;&lt;br&gt;
but &lt;code&gt;TypeScript&lt;/code&gt; treats it as if it were &lt;strong&gt;covariant&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which means some unsafe code compiles.&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;class&lt;/span&gt; &lt;span class="nc"&gt;Animal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;animal&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;class&lt;/span&gt; &lt;span class="nc"&gt;Cat&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Animal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;meow&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;class&lt;/span&gt; &lt;span class="nc"&gt;Dog&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Animal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;bark&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;cats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cat&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Cat&lt;/span&gt;&lt;span class="p"&gt;()];&lt;/span&gt;

&lt;span class="c1"&gt;// TS treats Cat[] as a subtype of Animal[]&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Animal&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cats&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Now this is allowed:&lt;/span&gt;
&lt;span class="nx"&gt;animals&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Dog&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// But the underlying array is still "cats"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cats&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="c1"&gt;// Type says Cat, but it's actually a Dog&lt;/span&gt;
&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;meow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Possible runtime error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why does TS allow this?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;From a type theory perspective, &lt;code&gt;T[]&lt;/code&gt; is a mutable container → should be invariant&lt;/li&gt;
&lt;li&gt;But &lt;code&gt;JavaScript&lt;/code&gt;’s arrays are extremely flexible and historically treated loosely&lt;/li&gt;
&lt;li&gt;Making &lt;code&gt;T[]&lt;/code&gt; strictly invariant would break a lot of existing &lt;code&gt;JavaScript&lt;/code&gt; code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;code&gt;TypeScript&lt;/code&gt; chose to keep &lt;code&gt;T[]&lt;/code&gt; almost &lt;strong&gt;covariant&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The recommended alternative is &lt;strong&gt;read-only arrays&lt;/strong&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;cats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;Cat&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Cat&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;animals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;Animal&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cats&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Safe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because we only read from a &lt;code&gt;readonly&lt;/code&gt; array, it can be safely covariant.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In practice: most generics in TS behave “mostly covariant”.&lt;br&gt;
The real danger is specifically “mutable but treated as covariant”, like &lt;code&gt;T[]&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Function parameters: should be contravariant, but often behave bivariantly
&lt;/h3&gt;

&lt;p&gt;The second big compromise: &lt;strong&gt;function parameter variance&lt;/strong&gt;.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;Function &lt;strong&gt;parameter&lt;/strong&gt; positions should be &lt;strong&gt;contravariant&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In practice, TypeScript often treats them as &lt;strong&gt;bivariant&lt;/strong&gt;, especially in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Methods in object types&lt;/li&gt;
&lt;li&gt;Some callback positions&lt;/li&gt;
&lt;li&gt;When &lt;code&gt;strictFunctionTypes&lt;/code&gt; is disabled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s see why that’s a problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why should parameters be contravariant?
&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;Handler&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="nx"&gt;value&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&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;T&lt;/code&gt; is used in &lt;strong&gt;parameter position&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It’s on the “receiving data” side&lt;/li&gt;
&lt;li&gt;Which means it should be &lt;strong&gt;contravariant&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If &lt;code&gt;Cat &amp;lt;: Animal&lt;/code&gt;, then (in theory):&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;Handler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Animal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Handler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Cat&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;We’ve already seen this pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  But TS sometimes allows both directions (bivariant)
&lt;/h3&gt;

&lt;p&gt;To maintain compatibility with existing &lt;code&gt;JavaScript&lt;/code&gt;, TS allows both directions in many callback cases:&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;Handler&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="nx"&gt;value&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&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;let&lt;/span&gt; &lt;span class="nx"&gt;handleEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Handler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Event&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;declare&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;handleMouse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Handler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MouseEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// In some contexts, TS allows:&lt;/span&gt;
&lt;span class="nx"&gt;handleEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;handleMouse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// OK&lt;/span&gt;
&lt;span class="nx"&gt;handleMouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;handleEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// OK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;With a purely sound type system, one of these would be rejected.&lt;br&gt;
TS chooses convenience over strict safety.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Consider this more dangerous version:&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;Handler&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="nx"&gt;value&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&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;handleEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Handler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Event&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="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="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="nx"&gt;event&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleMouse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Handler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MouseEvent&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="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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// MouseEvent-specific API&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Assign MouseEvent handler to a generic Event handler&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventHandler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Handler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Event&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;handleMouse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// This is allowed by the type system:&lt;/span&gt;
&lt;span class="nf"&gt;eventHandler&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;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// Runtime error: no clientX&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;strong&gt;should&lt;/strong&gt; be a type error, but bivariant behavior lets it through.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why did the TS team do this?
&lt;/h3&gt;

&lt;p&gt;Because real-world &lt;code&gt;JavaScript&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doesn’t assume strict contravariance&lt;/li&gt;
&lt;li&gt;Has tons of “loosely typed” callback APIs (&lt;code&gt;DOM&lt;/code&gt;, &lt;code&gt;Node.js&lt;/code&gt;, libraries, …)&lt;/li&gt;
&lt;li&gt;Would break massively if TS suddenly enforced full contravariance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the TS team made a pragmatic choice:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prioritize developer experience and compatibility&lt;br&gt;
over 100% soundness.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What about &lt;code&gt;strictFunctionTypes&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;There is a partial escape hatch:&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;"strict"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strictFunctionTypes"&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;With &lt;code&gt;strictFunctionTypes: true&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standalone function types&lt;/strong&gt; are treated more contravariantly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Methods in object types&lt;/strong&gt; are still treated more loosely (bivariantly) for compatibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So even in strict mode, you don’t get “perfect” contravariance — but you get closer.&lt;/p&gt;

&lt;h3&gt;
  
  
  🎯 Takeaway: &lt;code&gt;TypeScript&lt;/code&gt; is not a “pure variance lab”
&lt;/h3&gt;

&lt;p&gt;To summarize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Arrays &lt;code&gt;T[]&lt;/code&gt; should be invariant → TS treats them as almost covariant&lt;/li&gt;
&lt;li&gt;Function parameters should be contravariant → TS often treats them as bivariant&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;strictFunctionTypes&lt;/code&gt; helps, but doesn’t give you fully sound variance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;code&gt;TypeScript&lt;/code&gt; is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;not a perfectly sound type theory playground,&lt;br&gt;
but a pragmatic type system sitting on top of messy real-world JavaScript.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Variance in TS is “good enough” most of the time, but you should be aware of where it leaks.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌊 How Flow expresses variance explicitly
&lt;/h2&gt;

&lt;p&gt;Quick recap: &lt;code&gt;Flow&lt;/code&gt; is a static type checker for &lt;code&gt;JavaScript&lt;/code&gt;, originally developed at &lt;code&gt;Facebook&lt;/code&gt; (now &lt;code&gt;Meta&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Key characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More &lt;strong&gt;strict&lt;/strong&gt; and &lt;strong&gt;soundness-oriented&lt;/strong&gt; than TS&lt;/li&gt;
&lt;li&gt;Variance is &lt;strong&gt;explicitly&lt;/strong&gt; annotated&lt;/li&gt;
&lt;li&gt;Strong type inference&lt;/li&gt;
&lt;li&gt;Type annotations layered onto plain JS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In very rough terms:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;TypeScript&lt;/code&gt; focuses on practicality&lt;br&gt;
&lt;code&gt;Flow&lt;/code&gt; focuses more on safety&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One of Flow’s signature features is &lt;strong&gt;explicit variance annotations&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Variance annotations in Flow
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;Flow&lt;/code&gt;, you write variance directly on type parameters:&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;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;+T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;covariant&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;contravariant&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;invariant&lt;/strong&gt;(no sign)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So in &lt;code&gt;Flow&lt;/code&gt;, the author of a type explicitly declares:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This type parameter is used covariantly / contravariantly / invariantly.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In &lt;code&gt;TypeScript&lt;/code&gt;, the compiler mostly infers this.&lt;br&gt;
In &lt;code&gt;Flow&lt;/code&gt;, you annotate it, and Flow checks that your usage matches.&lt;/p&gt;


&lt;h2&gt;
  
  
  ⚛️ How React uses variance in Flow types
&lt;/h2&gt;

&lt;p&gt;React’s internal types were historically written in Flow, and you can still see traces of it:&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;export&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Element&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;+&lt;/span&gt;&lt;span class="nx"&gt;C&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;React$Element&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;C&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;export&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RefSetter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;I&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;React$RefSetter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;I&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;
&lt;code&gt;+C&lt;/code&gt; is covariant&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-I&lt;/code&gt; is contravariant&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why is Element&amp;lt;+C&amp;gt; covariant?
&lt;/h3&gt;

&lt;p&gt;Because &lt;code&gt;ReactElement&lt;/code&gt; is essentially &lt;strong&gt;immutable&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You construct it&lt;/li&gt;
&lt;li&gt;You read from it&lt;/li&gt;
&lt;li&gt;You don’t mutate its props in place&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So it’s safe to say that if &lt;code&gt;CatProps &amp;lt;: AnimalProps&lt;/code&gt;, then:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;ReactElement&amp;lt;CatProps&amp;gt; &amp;lt;: ReactElement&amp;lt;AnimalProps&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Example:&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AnimalProps&lt;/span&gt; &lt;span class="o"&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="kr"&gt;string&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;CatProps&lt;/span&gt; &lt;span class="o"&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;meow&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="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Cat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CatProps&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="kc"&gt;null&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&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;catElement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReactElement&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CatProps&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Cat&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"nyaomaru"&lt;/span&gt; &lt;span class="na"&gt;meow&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="o"&gt;=&amp;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;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Because of covariance, this is safe:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReactElement&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AnimalProps&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;catElement&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;A component that accepts “more specific props” can be used where “more general props” are expected&lt;/li&gt;
&lt;li&gt;The element is read-only — we’re not mutating its props through this type&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why is RefSetter&amp;lt;-I&amp;gt; contravariant?
&lt;/h3&gt;

&lt;p&gt;Because &lt;code&gt;RefSetter&lt;/code&gt; &lt;strong&gt;receives&lt;/strong&gt; values (instances) — it doesn’t produce them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It’s on the “write side”&lt;/li&gt;
&lt;li&gt;That’s a &lt;strong&gt;contravariant&lt;/strong&gt; position&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&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;HTMLDivRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLDivElement&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&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;HTMLElementRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have &lt;code&gt;HTMLDivElement &amp;lt;: HTMLElement&lt;/code&gt;, and with contravariance:&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;RefSetter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RefSetter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&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;So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A callback that can handle any &lt;code&gt;HTMLElement&lt;/code&gt; can safely be used where a “div-only” ref setter is expected&lt;/li&gt;
&lt;li&gt;But the opposite is unsafe: a &lt;code&gt;HTMLDivElement&lt;/code&gt;-only ref setter cannot safely accept any &lt;code&gt;HTMLElement&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matches exactly our earlier &lt;code&gt;Handler&amp;lt;T&amp;gt;&lt;/code&gt; examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters for reading React’s types
&lt;/h3&gt;

&lt;p&gt;Once you understand &lt;code&gt;Flow&lt;/code&gt;’s variance annotations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;+C&lt;/code&gt;(covariant) on &lt;code&gt;ReactElement&amp;lt;+C&amp;gt;&lt;/code&gt; → safe, immutable, read-only&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-T&lt;/code&gt; (contravariant) on &lt;code&gt;RefSetter&amp;lt;-T&amp;gt;&lt;/code&gt; → callback parameter, write-only&lt;/li&gt;
&lt;li&gt;No sign → invariant types where mutation may happen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes React’s Flow types much easier to reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ Where variance actually matters in real code
&lt;/h2&gt;

&lt;p&gt;This may still feel theoretical, but it absolutely shows up in day-to-day work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;onClick&lt;/code&gt;, &lt;code&gt;onChange&lt;/code&gt;, &lt;code&gt;onSubmit&lt;/code&gt; → UI event handlers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onSuccess&lt;/code&gt;, &lt;code&gt;onError&lt;/code&gt; → async/API callbacks&lt;/li&gt;
&lt;li&gt;Exposed “handler” or “listener” APIs in your own libraries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these are basically:&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;Handler&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="nx"&gt;value&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&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;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parameter types → &lt;strong&gt;contravariant&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Return types → &lt;strong&gt;covariant&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Mutable containers → &lt;strong&gt;invariant&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;TS callback parameters in methods → often &lt;strong&gt;bivariant&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When designing public APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prefer&lt;/strong&gt; wider types for parameters your consumers pass in&lt;/li&gt;
&lt;li&gt;Avoid exposing raw mutable containers like &lt;code&gt;Box&amp;lt;T&amp;gt;&lt;/code&gt; when a &lt;code&gt;ReadonlyBox&amp;lt;T&amp;gt;&lt;/code&gt; will do&lt;/li&gt;
&lt;li&gt;Consider using &lt;code&gt;readonly&lt;/code&gt; arrays and properties when you can&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Variance is a mental checklist for API design, not something you only care about in textbooks.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  📌 Wrap-up
&lt;/h2&gt;

&lt;p&gt;We covered a lot, so let’s distill the main points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Variance determines how subtyping of type parameters affects the outer type:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Covariant&lt;/strong&gt; → same direction (usually read-only)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contravariant&lt;/strong&gt; → opposite direction (usually function parameters)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invariant&lt;/strong&gt; → no subtyping either way (mutable containers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bivariant&lt;/strong&gt; → both directions (convenient but unsound)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;TypeScript:

&lt;ul&gt;
&lt;li&gt;Treats many generics as “mostly covariant”&lt;/li&gt;
&lt;li&gt;Makes &lt;code&gt;T[]&lt;/code&gt; effectively covariant (even though it should be invariant)&lt;/li&gt;
&lt;li&gt;Often treats function parameters as bivariant&lt;/li&gt;
&lt;li&gt;Provides &lt;code&gt;strictFunctionTypes&lt;/code&gt; to tighten some of this&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Flow:

&lt;ul&gt;
&lt;li&gt;Has explicit variance annotations (&lt;code&gt;+T&lt;/code&gt;, &lt;code&gt;-T&lt;/code&gt;, &lt;code&gt;T&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Practically:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;readonly&lt;/code&gt; vs mutable is a variance decision&lt;/li&gt;
&lt;li&gt;Callback parameter types are a variance decision&lt;/li&gt;
&lt;li&gt;Whether you expose &lt;code&gt;Box&amp;lt;T&amp;gt;&lt;/code&gt; or &lt;code&gt;ReadonlyBox&amp;lt;T&amp;gt;&lt;/code&gt; is a variance decision&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;You don’t need to memorize all the formal rules,&lt;br&gt;
but keeping “covariant / contravariant / invariant / bivariant” in your mental toolbox makes both &lt;strong&gt;reading&lt;/strong&gt; library types and &lt;strong&gt;designing&lt;/strong&gt; your own APIs much easier.&lt;/p&gt;

&lt;p&gt;I also made a small repo where you can play with these variance patterns in TypeScript:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/nyaomaru/variance-check" rel="noopener noreferrer"&gt;https://github.com/nyaomaru/variance-check&lt;/a&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%2F024tr0vk866y6rm3bkea.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%2F024tr0vk866y6rm3bkea.png" alt=" " width="800" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Have a nice variance life 🐈‍⬛✨&lt;/p&gt;




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

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Type_variance" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/Type_variance&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.typescriptlang.org/docs/handbook/2/generics.html#variance-annotations" rel="noopener noreferrer"&gt;https://www.typescriptlang.org/docs/handbook/2/generics.html#variance-annotations&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.totaltypescript.com/method-shorthand-syntax-considered-harmful" rel="noopener noreferrer"&gt;https://www.totaltypescript.com/method-shorthand-syntax-considered-harmful&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.sandromaglione.com/articles/covariant-contravariant-and-invariant-in-typescript" rel="noopener noreferrer"&gt;https://www.sandromaglione.com/articles/covariant-contravariant-and-invariant-in-typescript&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://zenn.dev/jay_es/articles/2024-02-13-typescript-variance" rel="noopener noreferrer"&gt;https://zenn.dev/jay_es/articles/2024-02-13-typescript-variance&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://typescriptbook.jp/reference/generics/variance" rel="noopener noreferrer"&gt;https://typescriptbook.jp/reference/generics/variance&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://effectivetypescript.com/2021/05/06/unsoundness/" rel="noopener noreferrer"&gt;https://effectivetypescript.com/2021/05/06/unsoundness/&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🐾 Shameless plug
&lt;/h2&gt;

&lt;p&gt;When you write &lt;code&gt;TypeScript&lt;/code&gt;, you probably end up writing &lt;code&gt;isXXX&lt;/code&gt; guards over and over.&lt;br&gt;
It’s boring and error-prone, so I built a small OSS library to help:&lt;/p&gt;

&lt;p&gt;👉 &lt;code&gt;is-kit&lt;/code&gt;: a tiny toolkit for building composable, type-safe 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%2Fkd71eb31m2x63vh1pzey.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%2Fkd71eb31m2x63vh1pzey.png" alt=" " width="800" height="571"&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;

&lt;p&gt;If you’re curious, I also wrote about it here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/nyaomaru/build-isxxx-the-easy-way-meet-is-kit-2hpl"&gt;https://dev.to/nyaomaru/build-isxxx-the-easy-way-meet-is-kit-2hpl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/nyaomaru/building-type-guards-like-lego-blocks-making-reusable-logic-with-is-kit-48e4"&gt;https://dev.to/nyaomaru/building-type-guards-like-lego-blocks-making-reusable-logic-with-is-kit-48e4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/nyaomaru/escaping-the-forest-of-if-statements-building-logical-type-guards-with-is-kit-2db3"&gt;https://dev.to/nyaomaru/escaping-the-forest-of-if-statements-building-logical-type-guards-with-is-kit-2db3&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>react</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Escaping the Forest of if Statements🌲: Building Logical Type Guards with `is-kit`</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Sun, 09 Nov 2025 10:11:05 +0000</pubDate>
      <link>https://dev.to/nyaomaru/escaping-the-forest-of-if-statements-building-logical-type-guards-with-is-kit-2db3</link>
      <guid>https://dev.to/nyaomaru/escaping-the-forest-of-if-statements-building-logical-type-guards-with-is-kit-2db3</guid>
      <description>&lt;p&gt;Hey everyone 👋&lt;br&gt;
I’m &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;@nyaomaru&lt;/a&gt;, a frontend engineer who stays warm every day by practicing sumo squats 🥶💪&lt;/p&gt;

&lt;p&gt;Have you ever written complex TypeScript code where type guards get tangled up inside a forest of nested if statements?&lt;/p&gt;

&lt;p&gt;Today, let’s explore how to escape that forest by composing type guards logically with the open-source 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;.&lt;/p&gt;

&lt;p&gt;Let’s dive in together!&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;
  
  
  🧠 The Philosophy of and, or, and not
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; provides composable logical operators for type guards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;and&lt;/code&gt;: all conditions must be true (intersection)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;or&lt;/code&gt;: at least one condition must be true (union)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;not&lt;/code&gt;: negates a condition (negation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These operators keep both runtime checks and &lt;strong&gt;TypeScript narrowing&lt;/strong&gt; intact.&lt;br&gt;
In other words, they turn type guards into a &lt;strong&gt;mini-DSL (domain-specific language)&lt;/strong&gt; for expressing logic in plain English.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;
  
  
  🌲 The Forest of if Statements
&lt;/h2&gt;

&lt;p&gt;When type guards pile up, your if statements tend to nest endlessly:&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;age&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;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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="s1"&gt;guest&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="s1"&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;function&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="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;User&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="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="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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="nx"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&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="k"&gt;return &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;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="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;isUser&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="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;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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="c1"&gt;// Admin, age 20+&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="c1"&gt;// Guest or trial user&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’ve all been there, a jungle of nested conditions 🌳🌳🌳&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  🏗️ Defining a Base Guard with &lt;code&gt;struct&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Let’s rewrite the User guard declaratively using &lt;code&gt;struct&lt;/code&gt;.&lt;br&gt;
Once you have this base guard, composing logic becomes a breeze.&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;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="nx"&gt;oneOfValues&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="s1"&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="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;age&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;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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="s1"&gt;guest&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="s1"&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="c1"&gt;// struct infers readonly properties; effectively Readonly&amp;lt;User&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;// This preserves structural type safety and helps prevent breaking changes in practice.&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="s1"&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="s1"&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="s1"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  ⚙️ Combining Guards with &lt;code&gt;and&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;and&lt;/code&gt; takes guards in order. First the base, then refinements.&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;narrowKeyTo&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="c1"&gt;// Readonly plus role fixed to the 'admin' literal&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AdminUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Readonly&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="o"&gt;&amp;amp;&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Reusable guard that narrows role to the 'admin' literal&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="s1"&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;isAdminUser&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="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// AdminUser&lt;/span&gt;

&lt;span class="c1"&gt;// Add age &amp;gt;= 18 as an additional refinement&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isAdultAdmin&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;isAdminUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;predicateToRefine&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AdminUser&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;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&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;candidate&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isAdultAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// candidate: AdminUser&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="nx"&gt;candidate&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="c1"&gt;// 'admin'&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;and&lt;/code&gt; guarantees short-circuit evaluation — if the first guard fails, the rest won’t run.&lt;br&gt;
Just like logical &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, but type-safe and composable.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;
  
  
  🔁 Combining Guards with &lt;code&gt;or&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;or&lt;/code&gt; accepts multiple guards and infers their union type 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="k"&gt;import&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;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="s1"&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;GuestUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Readonly&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="o"&gt;&amp;amp;&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guest&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;TrialUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Readonly&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="o"&gt;&amp;amp;&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trial&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="s1"&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="s1"&gt;guest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// GuestUser&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isTrial&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="s1"&gt;trial&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// TrialUser&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isGuestOrTrial&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;isGuest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isTrial&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;value&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isGuestOrTrial&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;// value is Readonly&amp;lt;User&amp;gt; &amp;amp; { role: 'guest' | 'trial' }&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="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;p&gt;&lt;code&gt;equalsBy&lt;/code&gt; safely compares a property based on a base guard.&lt;br&gt;
&lt;code&gt;or&lt;/code&gt; returns only the successful type — automatically narrowing &lt;code&gt;role&lt;/code&gt; to &lt;code&gt;'guest' | 'trial'&lt;/code&gt;.&lt;br&gt;
This type inference feels &lt;strong&gt;so satisfying&lt;/strong&gt; 😻&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;
  
  
  🚫 Negating Guards with &lt;code&gt;not&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;not&lt;/code&gt; lets you invert an existing guard or refinement.&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;not&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="s1"&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="c1"&gt;// Reuse definitions like isGuestOrTrial&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isGuestOnly&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;isGuestOrTrial&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;isTrial&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;x&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isGuestOnly&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="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="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 'guest'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conceptually, not mirrors TypeScript’s &lt;code&gt;Exclude&lt;/code&gt; type — letting you express “is not” logic naturally.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  🧩 Real-World Example: A Readable Logic DSL
&lt;/h2&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;narrowKeyTo&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="c1"&gt;// Builder for role literals&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="s1"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// admin and age 18 or older&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isAdultAdmin&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="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="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;predicateToRefine&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AdminUser&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;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// guest or trial&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isGuestOrTrial&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="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="s1"&gt;guest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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="s1"&gt;trial&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 turns messy conditionals into &lt;strong&gt;declarative, reusable logic&lt;/strong&gt;.&lt;br&gt;
Perfect for real-world readability and reuse.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;
  
  
  🛡️ Validating Safely with &lt;code&gt;safeParse&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When you want to return a guard’s result, &lt;code&gt;safeParse&lt;/code&gt; keeps things neat:&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;safeParse&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="s1"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adminCheck&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;isAdultAdmin&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;adminCheck&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;admin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;adminCheck&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="c1"&gt;// admin is Readonly&amp;lt;User&amp;gt; &amp;amp; { role: 'admin' }&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;handleAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;admin&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;guestCheck&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;isGuestOrTrial&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;guestCheck&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;guestCheck&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="c1"&gt;// member is Readonly&amp;lt;User&amp;gt; &amp;amp; { role: 'guest' | 'trial' }&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;handleGuestLike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;member&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;safeParse&lt;/code&gt; lets you write flow logic like a DSL.&lt;br&gt;
Clean, composable, and far from the if jungle 🌿&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  🧭 Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;and&lt;/code&gt; / &lt;code&gt;or&lt;/code&gt; / &lt;code&gt;not&lt;/code&gt; are type-safe logical operators with short-circuiting&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;struct&lt;/code&gt; and &lt;code&gt;oneOfValues&lt;/code&gt; boost reusability with declarative base guards&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;equalsBy&lt;/code&gt; and &lt;code&gt;andAll&lt;/code&gt; turn logic into readable, sentence-like expressions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;safeParse&lt;/code&gt; lets you safely return and reuse validated results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Time to graduate from &lt;code&gt;if (a &amp;amp;&amp;amp; b)&lt;/code&gt; and start speaking in &lt;code&gt;and(a, b)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you want to build snappy, composable type guards, try is-kit&lt;br&gt;
today!&lt;/p&gt;




&lt;p&gt;🌟 If you enjoyed this, drop a ⭐️ on the repo! It keeps me motivated! 😸&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;




&lt;p&gt;Please check tinyLaunch! 🚀&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tinylaunch.com/launch/6704" rel="noopener noreferrer"&gt;https://tinylaunch.com/launch/6704&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tooling</category>
      <category>tutorial</category>
      <category>typescript</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Building Type Guards Like LEGO Blocks: Making Reusable Logic with is-kit</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Wed, 15 Oct 2025 11:26:35 +0000</pubDate>
      <link>https://dev.to/nyaomaru/building-type-guards-like-lego-blocks-making-reusable-logic-with-is-kit-48e4</link>
      <guid>https://dev.to/nyaomaru/building-type-guards-like-lego-blocks-making-reusable-logic-with-is-kit-48e4</guid>
      <description>&lt;p&gt;Hey folks!&lt;/p&gt;

&lt;p&gt;Even in October, I’m still rocking short sleeves and shorts 😎&lt;br&gt;
I’m &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;@nyaomaru&lt;/a&gt;, a frontend engineer!&lt;/p&gt;

&lt;p&gt;When writing TypeScript, we often end up creating type guard functions (value is Foo) over and over again.&lt;/p&gt;

&lt;p&gt;But you know the pain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same &lt;code&gt;isXXX&lt;/code&gt; functions appearing everywhere&lt;/li&gt;
&lt;li&gt;Copy → tweak → repeat = infinite boilerplate hell&lt;/li&gt;
&lt;li&gt;Complex type gymnastics making maintenance painful&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sound familiar? 😅&lt;/p&gt;

&lt;p&gt;So today, I’ll show a pattern for turning your type guards into reusable logic.&lt;br&gt;
I’ll use my lightweight, zero-dependency library &lt;a href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;is-kit&lt;/a&gt; to illustrate the idea — but the mindset applies to vanilla TypeScript too.&lt;/p&gt;

&lt;p&gt;Alright, let’s dive in! 🧩&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;
  
  
  What’s a Type Guard Anyway?
&lt;/h2&gt;

&lt;p&gt;In short: &lt;strong&gt;a function that performs runtime validation while narrowing types&lt;/strong&gt; at the same 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="c1"&gt;// Checks if the argument is a string at runtime&lt;/span&gt;
&lt;span class="c1"&gt;// and narrows the type to string in the true branch&lt;/span&gt;
&lt;span class="kd"&gt;function&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="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&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="s1"&gt;string&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;Simple, right?&lt;br&gt;
Here’s a slightly more realistic version:&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;// A simple User type&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SimpleUser&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;number&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="c1"&gt;// Checks if a value is a SimpleUser&lt;/span&gt;
&lt;span class="kd"&gt;function&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="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;SimpleUser&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="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="s1"&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="k"&gt;typeof &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&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="k"&gt;typeof &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&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;That’s the usual &lt;code&gt;if (isUser(x))&lt;/code&gt; pattern — inside that block, &lt;code&gt;x.id&lt;/code&gt; is safely a number.&lt;/p&gt;

&lt;p&gt;Handy, but writing it manually every time is inefficient.&lt;br&gt;
It’s also hard to reuse or compose.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;
  
  
  ♻️ Build Type Guards Through “Definition” and “Composition”
&lt;/h2&gt;

&lt;p&gt;If you split type guards into smaller, reusable pieces, you can mix and match them across your project.&lt;/p&gt;

&lt;p&gt;Let’s make the earlier example more declarative.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example: Declare the User Shape
&lt;/h3&gt;

&lt;p&gt;The previous &lt;code&gt;isUser&lt;/code&gt; was fine, but not very reusable.&lt;br&gt;
Let’s define each &lt;code&gt;isXXX&lt;/code&gt; individually:&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;isObject&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;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&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="s1"&gt;object&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;function&lt;/span&gt; &lt;span class="nf"&gt;isNull&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;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;null&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="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="kd"&gt;function&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="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&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="s1"&gt;string&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;function&lt;/span&gt; &lt;span class="nf"&gt;isNumber&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;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&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="s1"&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;span class="kd"&gt;function&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="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;SimpleUser&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="nf"&gt;isObject&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="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isNull&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;isNumber&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;id&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;isString&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now each guard is modular — reusable, composable, and clean.&lt;/p&gt;

&lt;p&gt;If you need a stricter version of the object guard (to exclude arrays, dates, etc.), you can write:&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;// Checks for a plain object (not Array, Date, etc.)&lt;/span&gt;
&lt;span class="c1"&gt;// Inference: Record&amp;lt;string, unknown&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isPlainObject&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;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&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="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;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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="nx"&gt;proto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPrototypeOf&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;proto&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;||&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPrototypeOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proto&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;is-kit&lt;/code&gt;, the same logic 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="k"&gt;import&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;isString&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="s1"&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;isNumber&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;Clean, concise, and fully type-safe! ✨&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;struct&lt;/code&gt; infers &lt;code&gt;InferSchema&lt;/code&gt; automatically → &lt;code&gt;id: number&lt;/code&gt; and &lt;code&gt;name: string&lt;/code&gt; are guaranteed&lt;/li&gt;
&lt;li&gt;It only accepts plain objects, not Arrays or Dates&lt;/li&gt;
&lt;li&gt;You can harden boundaries via &lt;code&gt;struct(schema, { exact: true })&lt;/code&gt; to reject extra keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; gives you small building blocks (primitive guards) and composition utilities to build logic like LEGO.&lt;/p&gt;

&lt;h3&gt;
  
  
  📘 So Far
&lt;/h3&gt;

&lt;p&gt;Splitting guards into small &lt;code&gt;isXXX&lt;/code&gt; pieces makes them reusable.&lt;br&gt;
But in real-world code, we often need combinations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Add extra conditions” (AND)&lt;/li&gt;
&lt;li&gt;“Pass if any condition matches” (OR)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you chain those manually with &lt;code&gt;if&lt;/code&gt; statements… welcome to conditional hell.&lt;br&gt;
Let’s fix that.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;
  
  
  🛡️ Express Complex Guards Smartly
&lt;/h2&gt;

&lt;p&gt;Ever seen a snowballing &lt;code&gt;if&lt;/code&gt; statement mess?&lt;br&gt;
Like when a field must be even and start with &lt;code&gt;SP\_&lt;/code&gt; and have a specific property?&lt;/p&gt;

&lt;p&gt;Let’s walk through a before → after refactor.&lt;/p&gt;
&lt;h3&gt;
  
  
  Before: a tangled monster
&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;SimpleUser&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;number&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;type&lt;/span&gt; &lt;span class="nx"&gt;SpecialUser&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;number&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="nl"&gt;specialSetting&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;function&lt;/span&gt; &lt;span class="nf"&gt;isSpecialUser&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;SimpleUser&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;SpecialUser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;SpecialUser&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="nf"&gt;isObject&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="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isNull&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;isNumber&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;id&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;isString&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&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="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SP_&lt;/span&gt;&lt;span class="dl"&gt;'&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;isString&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;specialSetting&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;It works, but you’ll go mad maintaining it.&lt;/p&gt;
&lt;h3&gt;
  
  
  After: small reusable predicates
&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;UserBase&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;number&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;type&lt;/span&gt; &lt;span class="nx"&gt;SimpleUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UserBase&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;SpecialUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UserBase&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;specialSetting&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;function&lt;/span&gt; &lt;span class="nf"&gt;isEven&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;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;isNumber&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="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&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="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isSpecialName&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;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&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="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SP_&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;function&lt;/span&gt; &lt;span class="nf"&gt;isSpecialSetting&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;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&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="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isSpecialUser&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;SimpleUser&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;SpecialUser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;SpecialUser&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="nf"&gt;isObject&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="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isNull&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;isEven&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;id&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;isSpecialName&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nf"&gt;isSpecialSetting&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;specialSetting&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;Now it’s modular and clear — easy to extend or reuse elsewhere.&lt;/p&gt;
&lt;h3&gt;
  
  
  With is-kit
&lt;/h3&gt;

&lt;p&gt;We can express the same thing more elegantly:&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;guardIn&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;isString&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;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="s1"&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;isSimpleUser&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;isNumber&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isSpecialUser&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="nf"&gt;and&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="nf"&gt;predicateToRefine&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;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;id&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&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="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="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="nf"&gt;predicateToRefine&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="kr"&gt;string&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;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SP_&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;specialSetting&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;isSpecialUserInUnion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;guardIn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SimpleUser&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;SpecialUser&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;isSpecialUser&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then:&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="kr"&gt;declare&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SimpleUser&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;SpecialUser&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;isSpecialUserInUnion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;specialSetting&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="c1"&gt;// candidate: SpecialUser&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s all.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;struct&lt;/code&gt; handles the shape,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;and&lt;/code&gt; adds conditions,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;guardIn&lt;/code&gt; safely narrows within unions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Type-safe, declarative, and beautifully reusable 😸&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note:&lt;br&gt;
&lt;code&gt;guardIn&lt;/code&gt; helps safely narrow within unions,&lt;br&gt;
but even plain &lt;code&gt;if (isSpecialUser(x))&lt;/code&gt; often works fine —&lt;br&gt;
TypeScript is smart enough to infer it in many cases.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;/p&gt;

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

&lt;p&gt;Type guards don’t have to be one-off defense lines.&lt;br&gt;
They can be &lt;strong&gt;reusable logic blocks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;struct&lt;/code&gt; to define shape,&lt;br&gt;
&lt;code&gt;predicateToRefine&lt;/code&gt; to add constraints,&lt;br&gt;
and &lt;code&gt;and&lt;/code&gt; / &lt;code&gt;or&lt;/code&gt; to compose conditions —&lt;/p&gt;

&lt;p&gt;you can declare &lt;code&gt;isXXX&lt;/code&gt; functions that are short, safe, and shareable.&lt;/p&gt;

&lt;p&gt;Try converting just one of your existing &lt;code&gt;isXXX&lt;/code&gt; into an is-kit version.&lt;br&gt;
You’ll instantly feel the difference 🚀&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;

&lt;p&gt;If you like it, drop a ⭐ — it really helps! 😻&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>programming</category>
      <category>opensource</category>
      <category>coding</category>
    </item>
    <item>
      <title>My Favorite Frontend Setup Libraries (Project Foundation Edition)</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Wed, 08 Oct 2025 10:28:55 +0000</pubDate>
      <link>https://dev.to/nyaomaru/my-favorite-frontend-setup-libraries-project-foundation-edition-2j4k</link>
      <guid>https://dev.to/nyaomaru/my-favorite-frontend-setup-libraries-project-foundation-edition-2j4k</guid>
      <description>&lt;p&gt;Hey folks!&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 somehow always catches a cold when the seasons change 😿&lt;/p&gt;

&lt;p&gt;This time, I’ll introduce three libraries that power the everyday foundation of my development setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mise&lt;/code&gt; — Tool version management &amp;amp; task runner&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TypeDoc&lt;/code&gt; — API documentation generator&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tsd&lt;/code&gt; — Type testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alright, let’s dive in together!&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  mise: Unified version and task management for your tools
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/jdx/mise" rel="noopener noreferrer"&gt;https://github.com/jdx/mise&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First off, the README looks great — clean logo, nice demos.&lt;br&gt;
The docs are also well done:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://mise.jdx.dev/getting-started.html" rel="noopener noreferrer"&gt;https://mise.jdx.dev/getting-started.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Installation steps vary by OS, so just follow your platform’s guide.&lt;/p&gt;

&lt;p&gt;By the way, it’s pronounced “meez”, as in “mise-en-place”:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;mise (pronounced "meez") or "mise-en-place"&lt;br&gt;
&lt;a href="https://mise.jdx.dev/about.html" rel="noopener noreferrer"&gt;https://mise.jdx.dev/about.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  🎁 What it can do
&lt;/h3&gt;

&lt;p&gt;Setting up project environments becomes super easy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pin and auto-switch versions of &lt;code&gt;Node&lt;/code&gt;, &lt;code&gt;pnpm&lt;/code&gt;, &lt;code&gt;Bun&lt;/code&gt;, &lt;code&gt;Deno&lt;/code&gt;, &lt;code&gt;Python&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;Compatible with &lt;code&gt;.tool-versions&lt;/code&gt; (easy migration from nvm-style workflows)&lt;/li&gt;
&lt;li&gt;Built-in task runner (even replaces &lt;code&gt;make&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Personally, I’d call it a mix of &lt;code&gt;nvm&lt;/code&gt; + &lt;code&gt;makefile&lt;/code&gt; + α — everything nicely bundled in one tool. Love it.&lt;/p&gt;
&lt;h3&gt;
  
  
  🔎 How to use it
&lt;/h3&gt;

&lt;p&gt;Minimal setup (&lt;code&gt;mise.toml&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[tools]
node = "22.6.0"
pnpm = "9"
bun = "1"

[env]
NODE_ENV = "development"

[tasks.setup]
description = "Install dependencies via pnpm"
run = "pnpm install"

[tasks.dev]
description = "Boot application via pnpm"
run = "pnpm dev"

[tasks.lint]
description = "Run lint via pnpm"
run = "pnpm lint"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just place this file at the project root — that’s it!&lt;/p&gt;

&lt;h3&gt;
  
  
  🎯 Why I recommend it
&lt;/h3&gt;

&lt;p&gt;ecause it makes environment setup &lt;strong&gt;effortless&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sync &lt;code&gt;Node&lt;/code&gt; and &lt;code&gt;pnpm&lt;/code&gt; versions across the whole team instantly&lt;/li&gt;
&lt;li&gt;Centralize tasks → same commands work locally and in CI&lt;/li&gt;
&lt;li&gt;Save 2–3 seconds of context-switching every time you start work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Perfect for &lt;strong&gt;team-based projects&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The only mildly annoying part is that initial “Do you trust this repo?” prompt 😅&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%2Fjmcccw9zcao4osbm8ra9.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%2Fjmcccw9zcao4osbm8ra9.PNG" alt=" " width="561" height="86"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just hit “Yes,” and from then on it’s smooth sailing.&lt;br&gt;
Honestly, I can’t live without it now!&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;
  
  
  TypeDoc: Generate “truthful” API docs straight from your types
&lt;/h2&gt;

&lt;p&gt;Check out the official site first:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://typedoc.org/" rel="noopener noreferrer"&gt;https://typedoc.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That stylish auto-generated UI? Yep, all powered by TypeDoc.&lt;br&gt;
So thankful this is open source 🙏&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/TypeStrong/typedoc" rel="noopener noreferrer"&gt;https://github.com/TypeStrong/typedoc&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Easy to install and use!&lt;/p&gt;
&lt;h3&gt;
  
  
  🎁 What it can do
&lt;/h3&gt;

&lt;p&gt;Like &lt;code&gt;swagger-docs&lt;/code&gt;, it &lt;strong&gt;automatically generates documentation from your code&lt;/strong&gt;,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generated from actual exports → less chance of doc drift&lt;/li&gt;
&lt;li&gt;Monorepo-friendly: combine multiple packages into one unified site&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since it’s built from code itself, refactoring stays painless — no more stale wiki pages rotting in peace.&lt;/p&gt;
&lt;h3&gt;
  
  
  🔎 How to use it
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add &lt;span class="nt"&gt;-D&lt;/span&gt; typedoc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Minimal config (&lt;code&gt;typedoc.json&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "entryPoints": ["src/index.ts"],
  "out": "docs/api",
  "tsconfig": "tsconfig.json",
  "excludePrivate": true,
  "excludeInternal": true
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to your scripts for CI integration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "scripts": {
    "docs": "typedoc"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comments are reflected automatically, too:&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;// is-lit/define.ts&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Wraps a user function as a typed predicate.
 *
 * Note: The correctness of the predicate is the caller's responsibility.
 * Its result is coerced to a boolean using `!!` for consistent guard behavior.
 *
 * @param fn Function that returns truthy when the value matches the target shape.
 * @returns Predicate narrowing to the intended type when it returns true.
 */&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;define&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;fn&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;unknown&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;value&lt;/span&gt; &lt;span class="k"&gt;is&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;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="o"&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;define&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;fn&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;unknown&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;boolean&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="o"&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;define&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;fn&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;unknown&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;value&lt;/span&gt; &lt;span class="k"&gt;is&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="nx"&gt;value&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="o"&gt;=&amp;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;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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;value&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;value&lt;/span&gt; &lt;span class="k"&gt;is&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="nf"&gt;fn&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;✨ When you generate docs with typedoc, this JSDoc comment automatically appears as part of the API documentation,&lt;br&gt;
showing both the overload signatures and description in a clean, “truth-from-types” way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  🎯 Why I recommend it
&lt;/h3&gt;

&lt;p&gt;Because &lt;strong&gt;it’s automatic and low-maintenance&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Super easy to set up — works out of the box if your types are clean&lt;/li&gt;
&lt;li&gt;Publish on GitHub Pages → instantly shareable public docs&lt;/li&gt;
&lt;li&gt;Configurable via &lt;code&gt;typedoc.json&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;, or &lt;code&gt;tsconfig.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Perfect for &lt;strong&gt;sharing specs and APIs across teams&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Note: it requires Node.js, so pairing it with mise lets you run it as &lt;code&gt;mise run docs&lt;/code&gt; — easy peasy.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  tsd: Test your types with &lt;code&gt;.test-d.ts&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/tsdjs/tsd" rel="noopener noreferrer"&gt;https://github.com/tsdjs/tsd&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This one’s a &lt;strong&gt;type-level testing tool&lt;/strong&gt;.&lt;br&gt;
It’s especially useful for libraries or OSS — maybe overkill for regular apps,&lt;br&gt;
but really fun to try!&lt;/p&gt;
&lt;h3&gt;
  
  
  🎁 What it can do
&lt;/h3&gt;

&lt;p&gt;Run &lt;strong&gt;type tests&lt;/strong&gt; instead of runtime ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Execute &lt;code&gt;.test-d.ts&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;expectType&lt;/code&gt;, &lt;code&gt;expectError&lt;/code&gt;, &lt;code&gt;expectAssignable&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;Guarantees type behavior at compile time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also doubles as &lt;strong&gt;usage documentation&lt;/strong&gt; — your tests show how types are intended to be used.&lt;/p&gt;
&lt;h3&gt;
  
  
  🔎 How to use it
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add &lt;span class="nt"&gt;-D&lt;/span&gt; tsd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Add config to your &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"tsd": {
  "directory": "tests-d",
  "compilerOptions": {
    "baseUrl": "."
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;code&gt;tests-d&lt;/code&gt; directory with a &lt;code&gt;tsconfig.json&lt;/code&gt; inside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noEmit": true,
    "baseUrl": "..",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["./**/*.ts"],
  "exclude": ["../node_modules", "../dist"]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { expectType } from 'tsd';
import { define } from '../../src/core/define';
import type { Predicate } from '../../src/types';

// =============================================
// describe: define (types)
// =============================================
// it: returns a Predicate&amp;lt;T&amp;gt; regardless of input predicate flavor
expectType&amp;lt;Predicate&amp;lt;string&amp;gt;&amp;gt;(define&amp;lt;string&amp;gt;(() =&amp;gt; true));
expectType&amp;lt;Predicate&amp;lt;number&amp;gt;&amp;gt;(define&amp;lt;number&amp;gt;((x) =&amp;gt; typeof x === 'number'));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example comes from my own library: &lt;a href="https://github.com/nyaomaru/is-kit" rel="noopener noreferrer"&gt;is-kit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's a lightweight, dependency-free way to build safe &lt;code&gt;isXXX&lt;/code&gt; type guards 🚀&lt;/p&gt;

&lt;h3&gt;
  
  
  🎯 Why I recommend it
&lt;/h3&gt;

&lt;p&gt;Because &lt;strong&gt;testing types&lt;/strong&gt; is surprisingly fun:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensures type-level safety that often gets overlooked&lt;/li&gt;
&lt;li&gt;Verifies that your intended usage matches your design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;So that’s my shortlist of must-have project setup libraries for modern frontend development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mise&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;typedoc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tsd&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How about you? What are your favorite tools?&lt;/p&gt;

&lt;p&gt;Honestly, I didn’t write this just to say,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Look how cool these are 😎”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It’s more like,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I genuinely want to hear your recommendations!” 🙏&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So please drop your favorite libraries, articles, books, or even unrelated obsessions in the comments — I’d love to check them out!&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus
&lt;/h2&gt;

&lt;p&gt;I also made a tiny utility for building type-safe &lt;code&gt;isXXX&lt;/code&gt; guards — give it a look if you’re curious!&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%2Fk6a2e3p2d73v4kervpi6.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%2Fk6a2e3p2d73v4kervpi6.png" alt=" " width="800" height="571"&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>frontend</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>Build isXXX the Easy Way? Meet is-kit</title>
      <dc:creator>nyaomaru</dc:creator>
      <pubDate>Sun, 05 Oct 2025 12:03:10 +0000</pubDate>
      <link>https://dev.to/nyaomaru/build-isxxx-the-easy-way-meet-is-kit-2hpl</link>
      <guid>https://dev.to/nyaomaru/build-isxxx-the-easy-way-meet-is-kit-2hpl</guid>
      <description>&lt;p&gt;Hey folks! The leaves are about to turn 🍁&lt;/p&gt;

&lt;p&gt;I keep telling myself “just one more coffee”… but my hands are starting to shake ☕️😂&lt;br&gt;
Anyway, I’m &lt;a href="https://github.com/nyaomaru" rel="noopener noreferrer"&gt;@nyaomaru&lt;/a&gt;, a frontend engineer.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;
  
  
  Why I built this
&lt;/h2&gt;

&lt;p&gt;If you write TypeScript a lot, you’ve probably hand-written type guards like &lt;code&gt;value is Foo&lt;/code&gt; more times than you can count.&lt;br&gt;
And keeping runtime checks perfectly aligned with types… kinda exhausting.&lt;/p&gt;

&lt;p&gt;Enter &lt;code&gt;is-kit&lt;/code&gt; — a lightweight, zero-dependency &lt;strong&gt;“guard generator kit”&lt;/strong&gt; that slices through boring boilerplate ✂️&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%2Fbn3x34pvxwohonfh1s4q.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%2Fbn3x34pvxwohonfh1s4q.png" alt=" " width="800" height="571"&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;

&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;
  
  
  Why is-kit?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🖋 &lt;strong&gt;Stop hand-writing&lt;/strong&gt; &lt;code&gt;value is Foo&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Centralize guard definitions with define and predicateToRefine.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;⚙️ &lt;strong&gt;Compose logic with&lt;/strong&gt; &lt;code&gt;and&lt;/code&gt; / &lt;code&gt;or&lt;/code&gt; / &lt;code&gt;not&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Express domain-specific conditions declaratively.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;🧩 &lt;strong&gt;Combinators like&lt;/strong&gt; &lt;code&gt;struct&lt;/code&gt; / &lt;code&gt;array&lt;/code&gt; / &lt;code&gt;tuple&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Validate nested objects in just a few lines.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Runtime + type tests&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Confidence via Jest (runtime) and tsd (type assertions).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;
  
  
  How do I use it?
&lt;/h2&gt;

&lt;p&gt;A super common case: “non-empty string” without hand-rolling the predicate signature.&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="s1"&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;isNonEmptyString&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;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;isNonEmptyString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;foo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nf"&gt;isNonEmptyString&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="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By passing the generic &lt;code&gt;&amp;lt;string&amp;gt;&lt;/code&gt;, you don’t need to write the &lt;code&gt;value is string&lt;/code&gt; predicate signature yourself — clean and tidy.&lt;/p&gt;

&lt;p&gt;Another practical one: identifying a &lt;strong&gt;User&lt;/strong&gt; 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;import&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="nx"&gt;safeParseWith&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="s1"&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;isNumber&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="nf"&gt;isUser&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="mi"&gt;1&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="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parseUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;safeParseWith&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="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;parseUser&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="s1"&gt;abc&lt;/span&gt;&lt;span class="dl"&gt;'&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="mi"&gt;42&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// result.valid === false&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="c1"&gt;// on success&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// on failure&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 check whether API responses or form inputs satisfy your expected &lt;strong&gt;types safely&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For more complex conditions, compose:&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;or&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;predicateToRefine&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="s1"&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;isLongLiteral&lt;/span&gt; &lt;span class="o"&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="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;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&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;isLongString&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;isLongLiteral&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;isLongString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abcd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nf"&gt;isLongString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;span class="nf"&gt;isLongString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt; &lt;span class="k"&gt;as&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;// false&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;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;foo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// true&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;isString&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the combinators + primitives/object guards, you can express a wide range of isXXX guards cleanly.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Install &amp;amp; Ecosystem
&lt;/h2&gt;

&lt;p&gt;Install from npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add is-kit
&lt;span class="c"&gt;# or&lt;/span&gt;
bun add is-kit
&lt;span class="c"&gt;# or&lt;/span&gt;
npm i is-kit
&lt;span class="c"&gt;# or&lt;/span&gt;
yarn add is-kit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer JSR with Deno/Bun? Import TypeScript directly:&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;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="s1"&gt;jsr:@nyaomaru/is-kit&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; &lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; is a small kit for writing safe TypeScript guards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep it tiny with&lt;/strong&gt; &lt;code&gt;define&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model bigger shapes with&lt;/strong&gt; &lt;code&gt;struct&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combine complicated logic with&lt;/strong&gt; &lt;code&gt;and&lt;/code&gt; / &lt;code&gt;or&lt;/code&gt; / &lt;code&gt;not&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make ergonomics nice with&lt;/strong&gt; &lt;code&gt;safeParse&lt;/code&gt; / &lt;code&gt;safeParseWith&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re tired of drift between types and runtime checks, drop is-kit into your project and give it a spin. 🚀&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;

&lt;p&gt;And if you like it, a ⭐ would make my day! 😻&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>opensource</category>
      <category>util</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
