<?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: hiro@</title>
    <description>The latest articles on DEV Community by hiro@ (@hirodeath).</description>
    <link>https://dev.to/hirodeath</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F57464%2F7deedba2-079f-4a50-91e6-065b4e20a0c0.jpeg</url>
      <title>DEV Community: hiro@</title>
      <link>https://dev.to/hirodeath</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hirodeath"/>
    <language>en</language>
    <item>
      <title>useSyncExternalStore: test snapshot identity and the initial SSR value</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Mon, 02 Nov 2026 02:56:56 +0000</pubDate>
      <link>https://dev.to/hirodeath/usesyncexternalstore-test-snapshot-identity-and-the-initial-ssr-value-4240</link>
      <guid>https://dev.to/hirodeath/usesyncexternalstore-test-snapshot-identity-and-the-initial-ssr-value-4240</guid>
      <description>&lt;p&gt;An external store can notify its subscribers without changing the screen rendered through &lt;code&gt;useSyncExternalStore&lt;/code&gt;. After the same snapshot object was mutated in this experiment, the screen still displayed &lt;code&gt;0&lt;/code&gt; following the notification.&lt;/p&gt;

&lt;p&gt;The Hook needs a subscription notification and a snapshot that represents the change. With SSR, the initial client read must also receive the value used to generate the server HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  The notification arrived with the same snapshot reference
&lt;/h2&gt;

&lt;p&gt;This test store includes both replacement and a deliberately incorrect mutation for comparison. The &lt;code&gt;mutate&lt;/code&gt; method is not a recommended application pattern.&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createCounterStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initial&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;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;initial&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;listeners&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;Set&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;getSnapshot&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;snapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="k"&gt;for &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;listener&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;for &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;listener&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;listenerCount&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;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&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;&lt;code&gt;replace&lt;/code&gt; creates a new object. &lt;code&gt;mutate&lt;/code&gt; changes only &lt;code&gt;count&lt;/code&gt; on the existing object. Both call the registered listeners.&lt;/p&gt;

&lt;p&gt;The screen reads &lt;code&gt;count&lt;/code&gt; from &lt;code&gt;useSyncExternalStore(store.subscribe, store.getSnapshot)&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Count in the store&lt;/th&gt;
&lt;th&gt;Count on screen&lt;/th&gt;
&lt;th&gt;Recorded values passed through the display component&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initial&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mutate the same snapshot and notify&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;No additional invocation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replace with a new snapshot and notify&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2 added&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Checking only whether a notification was sent misses this difference. The value returned by &lt;code&gt;getSnapshot&lt;/code&gt; must represent the change when compared with &lt;code&gt;Object.is&lt;/code&gt;. The &lt;a href="https://react.dev/reference/react/useSyncExternalStore" rel="noopener noreferrer"&gt;official reference&lt;/a&gt; describes the contract: return the same snapshot while data is unchanged, and a new immutable snapshot when it changes.&lt;/p&gt;

&lt;p&gt;The test observed the mutation without an unrelated parent-state update or other rerender. It did not check what the display would show after a rerender triggered elsewhere.&lt;/p&gt;

&lt;p&gt;Returning a new object on every read violates the contract too. The documentation describes that pattern as causing an update loop, but this experiment did not execute that loop. It checks a store that reuses a snapshot until the next change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Include unsubscription in the store contract
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;subscribe&lt;/code&gt; returns a function that removes the listener as well as registering it. The experiment recorded one subscription after mount and 0 after unmount.&lt;/p&gt;

&lt;p&gt;The same store-provided &lt;code&gt;subscribe&lt;/code&gt; function is reused. This does not measure resubscription caused by creating a new function on every render or performance with many subscribers.&lt;/p&gt;

&lt;h2&gt;
  
  
  SSR needs a matching initial snapshot as well as a current value
&lt;/h2&gt;

&lt;p&gt;The server generated this HTML with &lt;code&gt;renderToString&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;output&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"store-count"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/output&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client store intentionally starts with a newer current value of &lt;code&gt;1&lt;/code&gt;. The server's initial &lt;code&gt;{ count: 0 }&lt;/code&gt; is passed through &lt;code&gt;window.__BOOTSTRAP__&lt;/code&gt; and read during hydration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&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;react&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;hydrateRoot&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;react-dom/client&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;createCounterStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;StoreCounter&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;./external-store.mjs&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;bootstrap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__BOOTSTRAP__&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;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCounterStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;count&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="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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;serverSnapshots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="na"&gt;renders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="na"&gt;errors&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;function&lt;/span&gt; &lt;span class="nf"&gt;getServerSnapshot&lt;/span&gt;&lt;span class="p"&gt;()&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;serverSnapshots&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;bootstrap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&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;bootstrap&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;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hydrateRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&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;StoreCounter&lt;/span&gt; &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;getServerSnapshot&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;getServerSnapshot&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;rendered&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;renders&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;value&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="nf"&gt;onRecoverableError&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="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errors&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;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;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readHydration&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="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="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;store-count&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subscribers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listenerCount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unmountHydration&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="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unmount&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;record&lt;/code&gt; and &lt;code&gt;rendered&lt;/code&gt; are instrumentation. They record snapshot reads and component invocations, while &lt;code&gt;onRecoverableError&lt;/code&gt; captures mismatches. The HTML and bootstrap data contain fixed numbers only; this is not an example of embedding user data.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Initial value passed to the client&lt;/th&gt;
&lt;th&gt;Sequence of count values read by the component&lt;/th&gt;
&lt;th&gt;Hydration mismatch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0, matching the server&lt;/td&gt;
&lt;td&gt;0 → 1&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2, differing from the server&lt;/td&gt;
&lt;td&gt;2 → 1&lt;/td&gt;
&lt;td&gt;Present&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With the matching bootstrap, the component first read the server's value and then moved to the client store's current value. Both cases eventually displayed &lt;code&gt;1&lt;/code&gt;, so inspecting only the final display would miss the mismatch. These records capture component invocations, not the number of frames for which &lt;code&gt;0&lt;/code&gt; was visible.&lt;/p&gt;

&lt;p&gt;Omitting &lt;code&gt;getServerSnapshot&lt;/code&gt; and rendering the same component through &lt;code&gt;renderToString&lt;/code&gt; threw an error requiring the initial snapshot. The &lt;a href="https://react.dev/reference/react/useSyncExternalStore#adding-support-for-server-rendering" rel="noopener noreferrer"&gt;SSR documentation&lt;/a&gt; requires the same data during server rendering and hydration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hook does not replace the storage implementation
&lt;/h2&gt;

&lt;p&gt;In this example, &lt;code&gt;createCounterStore&lt;/code&gt; stores the value. The Hook connects its subscription and snapshot to React. The experiment does not move all application-local state into an external store.&lt;/p&gt;

&lt;p&gt;The tests ran on September 11, 2026, with React and React DOM 19.3.0, Node.js 24.15.0, esbuild 0.28.2, macOS arm64, and Headless Chrome 152. Notification behavior used a production build; hydration used a development build. Neither used Strict Mode or React Compiler. Server rendering used Node.js &lt;code&gt;renderToString&lt;/code&gt;. Tearing during concurrent rendering and streaming SSR were outside the test scope.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/react-batch09/external-store.mjs" rel="noopener noreferrer"&gt;store and display component&lt;/a&gt;, &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/react-batch09/render-ssr.mjs" rel="noopener noreferrer"&gt;SSR input&lt;/a&gt;, and &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-09/browser-results.json" rel="noopener noreferrer"&gt;browser results&lt;/a&gt; let you inspect notifications, snapshot references, and server initial values separately.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>ssr</category>
      <category>testing</category>
    </item>
    <item>
      <title>Controlled input work depends on state placement: compare 10, 100, and 500 fields</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Sun, 01 Nov 2026 02:49:41 +0000</pubDate>
      <link>https://dev.to/hirodeath/controlled-input-work-depends-on-state-placement-compare-10-100-and-500-fields-3kfh</link>
      <guid>https://dev.to/hirodeath/controlled-input-work-depends-on-state-placement-compare-10-100-and-500-fields-3kfh</guid>
      <description>&lt;p&gt;Field count alone does not determine whether a form should use controlled or uncontrolled inputs. Even with controlled inputs, placing state in each field meant editing one field invoked only one input component.&lt;/p&gt;

&lt;p&gt;The experiment compared parent-owned values, field-local state, and DOM-owned values across forms with 10, 100, and 500 fields. Reducing repeated component work requires looking at state placement as well as whether inputs are controlled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Count component calls for the same input operation
&lt;/h2&gt;

&lt;p&gt;These are the three field components. &lt;code&gt;trace&lt;/code&gt; is an experiment counter, not application state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ControlledField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;index&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;onChange&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fieldRenders&lt;/span&gt;&lt;span class="o"&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`field-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onChange&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LocalField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fieldRenders&lt;/span&gt;&lt;span class="o"&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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setValue&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="nf"&gt;initialValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`field-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="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="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fieldChanges&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UncontrolledField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fieldRenders&lt;/span&gt;&lt;span class="o"&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`field-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;initialValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parent-owned version keeps a &lt;code&gt;values&lt;/code&gt; array in state, replaces the changed position, and passes values to every input. The local version updates state inside &lt;code&gt;LocalField&lt;/code&gt;. The uncontrolled version supplies an initial &lt;code&gt;defaultValue&lt;/code&gt; and reads values through FormData on submission.&lt;/p&gt;

&lt;p&gt;Field count and order stay fixed within each condition. The fixture uses position keys and does not delete, insert, or reorder fields.&lt;/p&gt;

&lt;p&gt;Each input has a distinct name. A browser fill operation replaced the first field's &lt;code&gt;field-0&lt;/code&gt; value with &lt;code&gt;edited&lt;/code&gt;. This was not a character-by-character typing test. Both controlled versions recorded one onChange call for that operation.&lt;/p&gt;

&lt;p&gt;The measurement is the additional component-function calls caused by the edit, excluding initial rendering.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State placement&lt;/th&gt;
&lt;th&gt;Fields&lt;/th&gt;
&lt;th&gt;Additional parent-form calls&lt;/th&gt;
&lt;th&gt;Additional input-component calls&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Parent-owned&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parent-owned&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parent-owned&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Field-local state&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Field-local state&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Field-local state&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOM-owned&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOM-owned&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOM-owned&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The production build used no React Compiler, memo, or Strict Mode. The table therefore compares the same unoptimized structure. Zero input-component calls does not mean the browser performs no input or painting work. Execution time, FPS, and memory use were not measured.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://react.dev/reference/react-dom/components/input" rel="noopener noreferrer"&gt;React input reference&lt;/a&gt; describes updating a controlled value in onChange and moving state to the part of the tree that needs it. The observed results also show that controlled inputs do not inherently require the entire parent to rerun.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify collected values too
&lt;/h2&gt;

&lt;p&gt;All 9 conditions produced as many FormData entries as there were fields, with &lt;code&gt;edited&lt;/code&gt; as the first value after editing. This did not aggregate all field-local values into parent React state simultaneously. It collected values already reflected in the DOM using the submission method chosen for this experiment.&lt;/p&gt;

&lt;p&gt;Immediate previews of all fields or validation across fields require an additional decision about where values are read. This fixture contains independent text inputs only. It excludes form libraries, IME composition, and asynchronous validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same reset button produced different outcomes
&lt;/h2&gt;

&lt;p&gt;After editing, pressing a &lt;code&gt;type="reset"&lt;/code&gt; button left &lt;code&gt;edited&lt;/code&gt; in both controlled versions. The uncontrolled version returned to &lt;code&gt;field-0&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;After HTML reset&lt;/th&gt;
&lt;th&gt;Explicit reset method used in the experiment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Parent state&lt;/td&gt;
&lt;td&gt;edited&lt;/td&gt;
&lt;td&gt;Set state to an array of initial values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Field-local state&lt;/td&gt;
&lt;td&gt;edited&lt;/td&gt;
&lt;td&gt;Change the form key to remount it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uncontrolled&lt;/td&gt;
&lt;td&gt;field-0&lt;/td&gt;
&lt;td&gt;Call form.reset()&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Changing the key in the field-local version tests whether remounting restores initial values. Remounting also recreates component state below that point. It should not be applied unchanged when a reset must preserve focus or other state.&lt;/p&gt;

&lt;p&gt;After an explicit reset, all three approaches showed &lt;code&gt;field-0&lt;/code&gt; in the first field. Treat native reset and React state initialization as separate operations, then verify reset behavior for the chosen value ownership. The &lt;a href="https://react.dev/reference/react-dom/components/form" rel="noopener noreferrer"&gt;form reference&lt;/a&gt; also covers event-based submission and reading values with FormData.&lt;/p&gt;

&lt;p&gt;The tests ran on September 11, 2026, with React and React DOM 19.3.0, Node.js 24.15.0, esbuild 0.28.2, macOS arm64, and Headless Chrome 152. The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/react-batch09/forms.jsx" rel="noopener noreferrer"&gt;complete form fixture&lt;/a&gt; and &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-09/browser-results.json" rel="noopener noreferrer"&gt;results for the 9 conditions&lt;/a&gt; are available. These results do not establish that 500 fields always require uncontrolled inputs. Choose with both the values React needs during editing and the submission/reset behavior in view.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>performance</category>
      <category>testing</category>
    </item>
    <item>
      <title>Carrying unfinished work between Plane cycles without closing the release</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Sat, 31 Oct 2026 02:56:25 +0000</pubDate>
      <link>https://dev.to/hirodeath/carrying-unfinished-work-between-plane-cycles-without-closing-the-release-4lf8</link>
      <guid>https://dev.to/hirodeath/carrying-unfinished-work-between-plane-cycles-without-closing-the-release-4lf8</guid>
      <description>&lt;p&gt;The implementation is ready at the end of the week, but mobile verification remains. Marking the work item Done just to close the weekly cycle would make that unfinished verification look complete.&lt;/p&gt;

&lt;p&gt;At the weekly boundary, preserve the work item's actual state and decide what to carry forward. A release module should retain the conditions for delivering its output. The end of a week and readiness to distribute a feature need separate states.&lt;/p&gt;

&lt;p&gt;The example is SquadNote's trial-participant feature. A read-only Plane inspection on September 11, 2026 found zero non-archived cycles and zero modules. The following is a proposed workflow built from real acceptance criteria and verification records, not a report of an established weekly process. No Plane settings or work states were changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The remaining work was mobile verification
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SQN-30&lt;/code&gt; covers trial-participant permissions, invitation URL compatibility, promotion to full membership, and ending a trial. Its acceptance conditions require consistent behavior on web, iOS, and Android. The ticket covers the development environment and automated tests; production rollout is explicitly outside its scope.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/circle-hub/blob/d116e8a343e8d9e7ddd3d1985efe590fa1901868/docs/testing/sqn-30-trial-participant-checklist.md" rel="noopener noreferrer"&gt;verification document&lt;/a&gt; at fixed commit &lt;code&gt;d116e8a&lt;/code&gt; contains completed web checks and unfinished mobile checks. Some mobile checks are marked complete, but the final item confirming completion of mobile verification remains unchecked.&lt;/p&gt;

&lt;p&gt;The next plan does not need to repeat the entire implementation. It needs to preserve the verified version and results, then identify what remains unchecked on which device. This article did not rerun those application checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep one work item for the work itself
&lt;/h2&gt;

&lt;p&gt;According to &lt;a href="https://docs.plane.so/core-concepts/cycles" rel="noopener noreferrer"&gt;Plane's cycle documentation&lt;/a&gt;, unfinished work items retain their state when a cycle ends and can be transferred to another cycle. A work item can belong to only one cycle at a time. The &lt;a href="https://docs.plane.so/core-concepts/modules" rel="noopener noreferrer"&gt;module documentation&lt;/a&gt; allows a work item to belong to multiple modules, such as a feature module and a release module.&lt;/p&gt;

&lt;p&gt;Keep one work item and relate it to the relevant period and deliverable. There is no need to create two identically named tickets solely for weekly and release tracking.&lt;/p&gt;

&lt;p&gt;However, putting only &lt;code&gt;SQN-30&lt;/code&gt; in a release module would leave a gap. Done on that ticket does not include production distribution. Tracking the release requires separate work for distribution and verification afterward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Proposed arrangement

Work item: SQN-30
  Scope: Trial-participant implementation and acceptance in development
  Weekly membership: This cycle → next cycle if needed

Proposed additional work: Distribution and post-distribution verification
  Scope: Define targets and procedure, then record results for the target version

Release module
  Related work: SQN-30 and the distribution/verification task
  Acceptance: Distribution to the agreed targets and completion of required checks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The additional task has no Plane ID because it has not been created. It remains a proposal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Carry the reason for unfinished work into the next plan
&lt;/h2&gt;

&lt;p&gt;Applying the current verification record to a hypothetical weekly review produces the following decisions. This is a walkthrough, not the result of operating the process for two weeks.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision point&lt;/th&gt;
&lt;th&gt;SQN-30&lt;/th&gt;
&lt;th&gt;Weekly handling&lt;/th&gt;
&lt;th&gt;Release handling&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;End of week 1, with mobile checks remaining&lt;/td&gt;
&lt;td&gt;Keep unfinished&lt;/td&gt;
&lt;td&gt;Close the week and select checks that can be performed next week&lt;/td&gt;
&lt;td&gt;Incomplete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End of week 2, if development acceptance is satisfied&lt;/td&gt;
&lt;td&gt;Can be Done&lt;/td&gt;
&lt;td&gt;Record completion in that week&lt;/td&gt;
&lt;td&gt;Incomplete while distribution work remains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After distribution and target-version checks&lt;/td&gt;
&lt;td&gt;Keep Done&lt;/td&gt;
&lt;td&gt;Record distribution results in the relevant week&lt;/td&gt;
&lt;td&gt;Compare against module acceptance before completing it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Repeatedly extending the first week would obscure what was accomplished within the original period. Closing that period and selecting remaining work again gives the plan and outcome a stable boundary for comparison.&lt;/p&gt;

&lt;p&gt;Carryover should also include a reason. Here, it could say that mobile permission displays and access restrictions remain to be checked, with the web results linked at a fixed commit. Confirm that the required devices will be available in the next week. Transferring everything, including work that cannot be performed then, turns the next plan into storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve the agreed scope when deciding completion
&lt;/h2&gt;

&lt;p&gt;Removing acceptance conditions merely because verification missed a date changes the intended deliverable. Adding store distribution to a development-only ticket creates the opposite problem: work satisfying the original scope can no longer close.&lt;/p&gt;

&lt;p&gt;If scope must change, record what moves to another release and how the current acceptance conditions change. Preserve a reason for work removed from a module too. Simply unlinking it makes the original plan indistinguishable from a plan that never included it.&lt;/p&gt;

&lt;p&gt;In this example, development acceptance and production rollout were separate from the beginning. Keeping that distinction allows &lt;code&gt;SQN-30&lt;/code&gt; to close when appropriate while leaving release preparation visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not change the facts to tidy the first weekly report
&lt;/h2&gt;

&lt;p&gt;The evidence here consists of existing ticket scope, a fixed verification document, and official behavior. It does not include a live cycle transfer experiment or measurements of module progress displays.&lt;/p&gt;

&lt;p&gt;An initial adoption could use one feature to check whether unfinished work remains identifiable as it moves into the next plan. Then check whether development completion and distribution completion can be read separately. For this case, record why the work remains and which device or behavior needs checking next.&lt;/p&gt;

</description>
      <category>plane</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>When Strict Mode reruns Effects: inspect subscription setup and cleanup</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Fri, 30 Oct 2026 03:10:31 +0000</pubDate>
      <link>https://dev.to/hirodeath/when-strict-mode-reruns-effects-inspect-subscription-setup-and-cleanup-1b0c</link>
      <guid>https://dev.to/hirodeath/when-strict-mode-reruns-effects-inspect-subscription-setup-and-cleanup-1b0c</guid>
      <description>&lt;p&gt;When Strict Mode logs an Effect setup twice, check whether cleanup ran between the setups and how many subscriptions remain. Reducing the setup count to one does not establish that cleanup works.&lt;/p&gt;

&lt;p&gt;A small subscription was compared in React 19.3.0 development and production builds. Initial behavior also differed between wrapping the root in Strict Mode and wrapping only part of the tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Count registration and removal without external communication
&lt;/h2&gt;

&lt;p&gt;The test subscription target is a Set. Setup registers a handler, and a local button invokes all registered handlers. There are no network connections or notification services.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Subscription&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cleanup&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&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="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&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="s2"&gt;`message:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;room&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;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&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="s2"&gt;`setup:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cleanup&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&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="s2"&gt;`cleanup:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cleanup&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;output&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"room"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;output&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;&lt;code&gt;trace.active&lt;/code&gt; contains registered handlers, and &lt;code&gt;trace.events&lt;/code&gt; records their order. The normal version uses &lt;code&gt;cleanup: true&lt;/code&gt;; the deliberately incomplete comparison uses &lt;code&gt;false&lt;/code&gt;. The room can change from A to B, and the root is unmounted at the end.&lt;/p&gt;

&lt;p&gt;Separate bundles were built with development and production values for &lt;code&gt;process.env.NODE_ENV&lt;/code&gt;. In the root-wide condition, &lt;code&gt;&amp;lt;StrictMode&amp;gt;&lt;/code&gt; wraps the element rendered by &lt;code&gt;createRoot&lt;/code&gt;. In the partial condition, it wraps only &lt;code&gt;Subscription&lt;/code&gt; beneath a parent outside Strict Mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Placement changed initial setup and cleanup
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Build and placement&lt;/th&gt;
&lt;th&gt;Initial setups&lt;/th&gt;
&lt;th&gt;Initial cleanups&lt;/th&gt;
&lt;th&gt;Handlers remaining after initial processing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Development, root Strict Mode, with cleanup&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Development, no Strict Mode, with cleanup&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Development, child-only Strict Mode, with cleanup&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production, root Strict Mode, with cleanup&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Development, root Strict Mode, without cleanup&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The normal development root logged &lt;code&gt;setup:A → cleanup:A → setup:A&lt;/code&gt; during initial processing. It removed the earlier subscription before setting it up again.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://react.dev/reference/react/StrictMode" rel="noopener noreferrer"&gt;official Strict Mode documentation&lt;/a&gt; distinguishes the extra development setup/cleanup cycle from the condition where initial Effects are not rerun because Strict Mode does not wrap the root. Check where Strict Mode is placed before interpreting repeated Effect logs.&lt;/p&gt;

&lt;p&gt;The table counts Effect work. It does not establish component-function invocation counts or DOM painting counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Omitting cleanup accumulated handlers
&lt;/h2&gt;

&lt;p&gt;Pressing the local notification button once after initial setup logged one &lt;code&gt;message:A&lt;/code&gt; with cleanup and 2 without it. The next step changed the room to B.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Development root condition&lt;/th&gt;
&lt;th&gt;After initial registration&lt;/th&gt;
&lt;th&gt;After changing room&lt;/th&gt;
&lt;th&gt;After unmount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;With cleanup&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Without cleanup&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The normal version logged &lt;code&gt;cleanup:A → setup:B&lt;/code&gt; on the room change and &lt;code&gt;cleanup:B&lt;/code&gt; on unmount. Without cleanup, the A handlers remained while a B handler was added. They stayed in the Set after unmount too.&lt;/p&gt;

&lt;p&gt;The comparison shows what remains when the implementation omits its removal step. Each case discards its Set; the incomplete version was not integrated into an application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the matching teardown before limiting calls
&lt;/h2&gt;

&lt;p&gt;For this example, the fix is to remove the same handler that setup registered. A room change removes the old room's subscription before registering the new one. The &lt;a href="https://react.dev/reference/react/useEffect" rel="noopener noreferrer"&gt;useEffect reference&lt;/a&gt; describes cleanup with previous values followed by setup with the new values when dependencies change.&lt;/p&gt;

&lt;p&gt;This experiment does not apply the extra-execution scenario to API writes, payments, or email delivery. Its scope is a subscription with matching registration and removal. If real logs differ, investigate actual unmounts and remounts or development-tool updates as well as Strict Mode placement.&lt;/p&gt;

&lt;p&gt;The tests ran on September 11, 2026, with React and React DOM 19.3.0, Node.js 24.15.0, esbuild 0.28.2, macOS arm64, and Headless Chrome 152 and no React Compiler. The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/react-batch09/effects.jsx" rel="noopener noreferrer"&gt;fixture code&lt;/a&gt; and &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-09/browser-results.json" rel="noopener noreferrer"&gt;results for the 5 conditions&lt;/a&gt; include room changes and unmounts as well as initial processing.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>testing</category>
    </item>
    <item>
      <title>How index keys affect inputs: compare DOM, row state, and parent state</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Thu, 29 Oct 2026 03:06:24 +0000</pubDate>
      <link>https://dev.to/hirodeath/how-index-keys-affect-inputs-compare-dom-row-state-and-parent-state-bgn</link>
      <guid>https://dev.to/hirodeath/how-index-keys-affect-inputs-compare-dom-row-state-and-parent-state-bgn</guid>
      <description>&lt;p&gt;When the first row is removed from a React list, do input values stay with the remaining rows? With index keys, the result depended on who owned the value.&lt;/p&gt;

&lt;p&gt;An uncontrolled input, whose value lived in the DOM, and an input backed by row-local state displayed another row's value. Inputs receiving values from parent state displayed the correct values, but their DOM nodes were still reassigned to different rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Narrow the reproduction from existing code
&lt;/h2&gt;

&lt;p&gt;The personal project's &lt;a href="https://github.com/takahiro-saeki/circle-hub/blob/770de5f2989775cfd95f7a9c4529565a2b48d2fd/apps/web/src/app/%28app%29/organizations/%5Bid%5D/schedules/new/page.tsx" rel="noopener noreferrer"&gt;schedule creation screen&lt;/a&gt; passes the &lt;code&gt;idx&lt;/code&gt; from &lt;code&gt;rows.map&lt;/code&gt; as each &lt;code&gt;Card&lt;/code&gt; key. It removes rows with array filtering and inserts duplicates by combining array slices.&lt;/p&gt;

&lt;p&gt;However, its title input is controlled through &lt;code&gt;value={row.title}&lt;/code&gt;. The code alone does not show whether input values ever became mixed up in production. The inspected commit was &lt;code&gt;770de5f&lt;/code&gt;. Related history and design material were also checked before extracting row deletion and value ownership into a separate experiment. The application was neither modified nor used to submit data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Removing the first row shifts index-based identities
&lt;/h2&gt;

&lt;p&gt;The fixture starts with 3 rows: A, B, and C. B is edited to &lt;code&gt;edited-B&lt;/code&gt;, then A is removed. Simply reversing the order after editing B would leave B in the middle of a 3-row list, so deletion makes the identity change visible here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&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;react&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;initialRows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="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;A&lt;/span&gt;&lt;span class="dl"&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;Alpha&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;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;B&lt;/span&gt;&lt;span class="dl"&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;Beta&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;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;C&lt;/span&gt;&lt;span class="dl"&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;Gamma&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;Row&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;row&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="nx"&gt;update&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;localTitle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLocalTitle&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="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&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;props&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;uncontrolled&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;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&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;local&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;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;localTitle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setLocalTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;data-row&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&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;KeyExperiment&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;keyMode&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="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;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setRows&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="nx"&gt;initialRows&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;update&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="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;row&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="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&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;&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;button&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"remove-first"&lt;/span&gt; &lt;span class="na"&gt;onClick&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="nf"&gt;setRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Remove first&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Row&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;keyMode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;row&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;update&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="si"&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;&lt;code&gt;mode&lt;/code&gt; is &lt;code&gt;uncontrolled&lt;/code&gt;, &lt;code&gt;local&lt;/code&gt;, or &lt;code&gt;parent&lt;/code&gt;; &lt;code&gt;keyMode&lt;/code&gt; is &lt;code&gt;index&lt;/code&gt; or &lt;code&gt;id&lt;/code&gt;. The local version initializes each row with &lt;code&gt;useState(row.title)&lt;/code&gt;. The parent version updates the array by row ID.&lt;/p&gt;

&lt;p&gt;Before deletion, a WeakMap records each input's DOM node and row ID. After deletion, the test compares values and node origins. This is fixture instrumentation, not access to React's internal keys.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value owner&lt;/th&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Values displayed for B / C after deletion&lt;/th&gt;
&lt;th&gt;Original rows of the nodes used for B / C&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DOM&lt;/td&gt;
&lt;td&gt;index&lt;/td&gt;
&lt;td&gt;Alpha / edited-B&lt;/td&gt;
&lt;td&gt;A / B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOM&lt;/td&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;edited-B / Gamma&lt;/td&gt;
&lt;td&gt;B / C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row state&lt;/td&gt;
&lt;td&gt;index&lt;/td&gt;
&lt;td&gt;Alpha / edited-B&lt;/td&gt;
&lt;td&gt;A / B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row state&lt;/td&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;edited-B / Gamma&lt;/td&gt;
&lt;td&gt;B / C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parent state&lt;/td&gt;
&lt;td&gt;index&lt;/td&gt;
&lt;td&gt;edited-B / Gamma&lt;/td&gt;
&lt;td&gt;A / B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parent state&lt;/td&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;edited-B / Gamma&lt;/td&gt;
&lt;td&gt;B / C&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With index keys, B gets key &lt;code&gt;0&lt;/code&gt; and C gets key &lt;code&gt;1&lt;/code&gt; after deletion. Those keys previously corresponded to A and B. &lt;code&gt;defaultValue&lt;/code&gt; and a row's initial state are not reapplied from scratch for every new row occupying that position.&lt;/p&gt;

&lt;p&gt;With ID keys, B remains key &lt;code&gt;B&lt;/code&gt; even after moving. Its edited value and DOM node stayed with that row. &lt;a href="https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key" rel="noopener noreferrer"&gt;React's explanation of list keys&lt;/a&gt; describes their role in matching items through deletion, insertion, and reordering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Controlled values do not settle the identity question
&lt;/h2&gt;

&lt;p&gt;In the parent-state version, React passed the new &lt;code&gt;row.title&lt;/code&gt; to each input, so the displayed values were correct. The 6 cases therefore do not support a claim that index keys always produce incorrect displayed values.&lt;/p&gt;

&lt;p&gt;The WeakMap still identified the reused nodes as coming from A / B. Correct values alone do not prove that row, component, and DOM identities were preserved. The experiment did not measure focus, IME composition, or DatePicker internal state. It therefore does not establish failures in those parts of the real application either.&lt;/p&gt;

&lt;p&gt;For a design that adds and removes rows, storing a row ID in the data and keeping it as the key for the row's lifetime expresses the intended identity. Unsaved rows can receive an ID when created, and a duplicate treated as a separate row can receive a new ID. Generating IDs during every render would not provide stable matching. This is a proposed change for the existing screen, not an applied fix.&lt;/p&gt;

&lt;p&gt;The tests ran on September 11, 2026, with React and React DOM 19.3.0, Node.js 24.15.0, esbuild 0.28.2, macOS arm64, and Headless Chrome 152. They used a production build without Strict Mode or React Compiler. The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/react-batch09/keys.jsx" rel="noopener noreferrer"&gt;minimal code&lt;/a&gt; and &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-09/browser-results.json" rel="noopener noreferrer"&gt;results for the 6 conditions&lt;/a&gt; track displayed values and DOM nodes separately.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>testing</category>
    </item>
    <item>
      <title>Cancelling timers with AbortController: seven cases including cleanup</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Wed, 28 Oct 2026 02:45:42 +0000</pubDate>
      <link>https://dev.to/hirodeath/cancelling-timers-with-abortcontroller-seven-cases-including-cleanup-1g7c</link>
      <guid>https://dev.to/hirodeath/cancelling-timers-with-abortcontroller-seven-cases-including-cleanup-1g7c</guid>
      <description>&lt;p&gt;&lt;code&gt;AbortController&lt;/code&gt; is not limited to fetch. Calling &lt;code&gt;abort()&lt;/code&gt; does not stop an arbitrary Promise, though. The operation must accept the signal and implement both stopping its wait and rejecting its Promise.&lt;/p&gt;

&lt;p&gt;A small timer function was tested with cancellation before it started, while it was waiting, and after completion. The check covered not only rejection, but also whether abort listeners remained after the operation ended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement a delay that handles cancellation
&lt;/h2&gt;

&lt;p&gt;Save this function as &lt;code&gt;cancellable-delay.mjs&lt;/code&gt;. It is an implementation for this experiment, not code already integrated into an existing application.&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&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;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cleanup&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="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abort&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onAbort&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;onAbort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&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;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finished&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;ms&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abort&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onAbort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;once&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the signal is already aborted, the function immediately rejects with its reason. Otherwise it registers a timer and an abort listener. On cancellation, it clears the timer, removes the listener, and rejects.&lt;/p&gt;

&lt;p&gt;Normal completion also removes the listener. &lt;code&gt;{ once: true }&lt;/code&gt; removes a listener after an abort event occurs; it does not replace cleanup for an operation that finishes normally.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://dom.spec.whatwg.org/#using-abortcontroller-and-abortsignal-objects-in-apis" rel="noopener noreferrer"&gt;DOM requirements for abortable APIs&lt;/a&gt; describe checking whether the signal is already aborted when starting and rejecting an unfinished operation with the abort reason. This function follows that flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop multiple timers with the same signal
&lt;/h2&gt;

&lt;p&gt;Place this executable example in the same directory:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;delay&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;./cancellable-delay.mjs&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;controller&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;AbortController&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;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&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;reason&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;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screen closed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reason&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;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;group&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;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;status&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;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;reason&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It printed &lt;code&gt;[ 'rejected', 'rejected' ]&lt;/code&gt; followed by &lt;code&gt;true&lt;/code&gt;. Both timers received the same signal and rejected with the same &lt;code&gt;Error&lt;/code&gt; object. &lt;code&gt;Promise.allSettled&lt;/code&gt; collected their outcomes; the abort handling inside &lt;code&gt;delay&lt;/code&gt; stopped the timers.&lt;/p&gt;

&lt;p&gt;The example creates the aggregate Promise before cancelling. This establishes a path to receive rejection outcomes before calling abort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify cleanup under 7 conditions
&lt;/h2&gt;

&lt;p&gt;The tests ran on September 11, 2026, with macOS arm64 and Node.js 24.15.0. Node.js &lt;code&gt;getEventListeners&lt;/code&gt; was used to inspect abort listener counts for the custom function.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Observation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Abort before calling the function&lt;/td&gt;
&lt;td&gt;Rejected with the same reason; 0 remaining abort listeners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abort 2 timers sharing a signal&lt;/td&gt;
&lt;td&gt;Both rejected; registered listeners went from 2 to 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abort after normal completion&lt;/td&gt;
&lt;td&gt;Still fulfilled; listener count was already 0 at completion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Include an operation without a signal&lt;/td&gt;
&lt;td&gt;Only the cooperative operation rejected; the other completed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abort a Node.js Promise-based timer&lt;/td&gt;
&lt;td&gt;Rejected with AbortError; its &lt;code&gt;cause&lt;/code&gt; was the supplied reason&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Combine a manual signal and timeout with &lt;code&gt;AbortSignal.any&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Custom delay rejected with the manual abort reason&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Let only the timeout fire&lt;/td&gt;
&lt;td&gt;Custom delay rejected with TimeoutError&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The waiting cases registered 10000-millisecond timers before cancellation. Normal completion used 0 milliseconds; the firing timeout used 1 millisecond. These values drive state transitions. They are not measurements of execution time or cancellation latency.&lt;/p&gt;

&lt;p&gt;The custom delay cleared its timer after rejection, and the Node.js process exited. Together with the listener checks, this confirmed that the implementation did not merely reject its Promise while leaving a long-running timer behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  APIs can expose cancellation reasons differently
&lt;/h2&gt;

&lt;p&gt;The custom delay passes &lt;code&gt;signal.reason&lt;/code&gt; directly to reject. In contrast, the &lt;code&gt;node:timers/promises&lt;/code&gt; case supplied an &lt;code&gt;Error&lt;/code&gt; as the reason but received a separate &lt;code&gt;AbortError&lt;/code&gt; whose &lt;code&gt;cause&lt;/code&gt; was the original reason.&lt;/p&gt;

&lt;p&gt;Node.js &lt;a href="https://nodejs.org/download/release/v24.15.0/docs/api/timers.html#cancelling-timers" rel="noopener noreferrer"&gt;Promise-based timers&lt;/a&gt; already support AbortSignal cancellation. They are an option for waits specific to Node. The custom implementation here makes the operation's cleanup responsibilities visible.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AbortSignal.any&lt;/code&gt; combines signals and takes the reason from the first signal that aborts. Check support in the target runtime for it and &lt;code&gt;AbortSignal.timeout&lt;/code&gt;. Both are covered in the &lt;a href="https://nodejs.org/download/release/v24.15.0/docs/api/globals.html#class-abortsignal" rel="noopener noreferrer"&gt;Node.js AbortSignal reference&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  An operation without the signal continues
&lt;/h2&gt;

&lt;p&gt;One of the 7 conditions paired a cancellation-aware delay with a Promise-based timer that received no signal. After abort, the first rejected, but the second's completion flag became &lt;code&gt;true&lt;/code&gt;. Having a controller available in a shared variable does not enroll an operation in cancellation.&lt;/p&gt;

&lt;p&gt;This experiment also did not test forcibly stopping a synchronous loop that occupies the CPU. Nor does it establish that cancelling a network request rolls back a remote server's work or previously stored data. It verifies cooperative signal handling for waits in the same process.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/language-batch08/cancellable-delay.mjs" rel="noopener noreferrer"&gt;custom function&lt;/a&gt;, &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/language-batch08/run-runtime.mjs" rel="noopener noreferrer"&gt;test code&lt;/a&gt;, and &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-08/abort-results.json" rel="noopener noreferrer"&gt;results for the 7 conditions&lt;/a&gt; are available. When adding cancellation, check the initial aborted state, resource release on cancellation, and listener removal on normal completion as parts of the same operation.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>async</category>
      <category>testing</category>
    </item>
    <item>
      <title>Comparing structuredClone and JSON copies through values and reference identity</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Tue, 27 Oct 2026 02:36:56 +0000</pubDate>
      <link>https://dev.to/hirodeath/comparing-structuredclone-and-json-copies-through-values-and-reference-identity-2noe</link>
      <guid>https://dev.to/hirodeath/comparing-structuredclone-and-json-copies-through-values-and-reference-identity-2noe</guid>
      <description>&lt;p&gt;Copying an object with &lt;code&gt;JSON.parse(JSON.stringify(value))&lt;/code&gt; can succeed while changing what the data means. In this experiment, the changes went beyond turning a Date into a string: Map contents and relationships between references changed too.&lt;/p&gt;

&lt;p&gt;The two approaches were compared on Node.js 24.15.0. Specify which values need copying and which reference relationships the copy must retain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check values and references together
&lt;/h2&gt;

&lt;p&gt;This example uses synthetic data resembling an article draft, not real user records. The experiment ran on September 11, 2026. The JSON roundtrip used no replacer, reviver, or custom &lt;code&gt;toJSON&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ready&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;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-09-11T00:00:00.000Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;counts&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;Map&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]]),&lt;/span&gt;
  &lt;span class="na"&gt;memo&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;slots&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="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&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;cloned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;structuredClone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&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;jsonCopy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&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;cloned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Date&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;jsonCopy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&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;cloned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;jsonCopy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;counts&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="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasOwn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cloned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;memo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;hasOwn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jsonCopy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;memo&lt;/span&gt;&lt;span class="dl"&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;cloned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slots&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;jsonCopy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slots&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;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;cloned&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="nx"&gt;cloned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jsonCopy&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="nx"&gt;jsonCopy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It printed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;true string
2 {}
true false
undefined null
true false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structured clone's Date and Map remained usable as those types. The JSON copy contained a string for the Date and an empty object for the Map. Its &lt;code&gt;memo: undefined&lt;/code&gt; property disappeared, while &lt;code&gt;undefined&lt;/code&gt; inside the array became &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The final line matters for some copying requirements. Originally, &lt;code&gt;left&lt;/code&gt; and &lt;code&gt;right&lt;/code&gt; referenced the same &lt;code&gt;row&lt;/code&gt;. The structured clone kept them pointing at the same copied object. The JSON roundtrip made separate objects. Neither approach retained a reference to the original &lt;code&gt;row&lt;/code&gt;. The &lt;a href="https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal" rel="noopener noreferrer"&gt;HTML structured serialization specification&lt;/a&gt; defines bookkeeping that avoids processing an object twice and preserves cycles and reference identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Success alone does not establish the intended copy semantics
&lt;/h2&gt;

&lt;p&gt;The experiment tested 12 separate inputs. These are representative results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;structuredClone&lt;/th&gt;
&lt;th&gt;JSON roundtrip&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Date&lt;/td&gt;
&lt;td&gt;Date&lt;/td&gt;
&lt;td&gt;ISO-formatted string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Map&lt;/td&gt;
&lt;td&gt;Map containing entries&lt;/td&gt;
&lt;td&gt;Empty object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;undefined in an object&lt;/td&gt;
&lt;td&gt;Key and value retained&lt;/td&gt;
&lt;td&gt;Key removed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;undefined in an array&lt;/td&gt;
&lt;td&gt;undefined element&lt;/td&gt;
&lt;td&gt;null element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NaN, Infinity, -0&lt;/td&gt;
&lt;td&gt;Properties of each value retained&lt;/td&gt;
&lt;td&gt;null, null, 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two properties referencing the same child&lt;/td&gt;
&lt;td&gt;Copied properties share a child&lt;/td&gt;
&lt;td&gt;Copied properties have separate children&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Circular reference&lt;/td&gt;
&lt;td&gt;Cycle retained in the copy&lt;/td&gt;
&lt;td&gt;TypeError&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object containing BigInt&lt;/td&gt;
&lt;td&gt;bigint value retained&lt;/td&gt;
&lt;td&gt;TypeError&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object with a function property&lt;/td&gt;
&lt;td&gt;DataCloneError&lt;/td&gt;
&lt;td&gt;Function key removed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The JSON cases that removed a function or undefined property did not throw. A test that checks only for exceptions misses those changes. The &lt;a href="https://tc39.es/ecma262/multipage/structured-data.html#sec-json.stringify" rel="noopener noreferrer"&gt;ECMAScript JSON specification&lt;/a&gt; also distinguishes serialization by value type and by its position in an object or array.&lt;/p&gt;

&lt;p&gt;The comparison used &lt;code&gt;instanceof&lt;/code&gt;, &lt;code&gt;Object.hasOwn&lt;/code&gt;, &lt;code&gt;Object.is&lt;/code&gt;, and reference &lt;code&gt;===&lt;/code&gt; checks. Comparing only JSON strings cannot test properties that serialization itself removes.&lt;/p&gt;

&lt;h2&gt;
  
  
  structuredClone does not fully duplicate class instances
&lt;/h2&gt;

&lt;p&gt;The remaining 3 cases examined a class, frozen state, and a getter.&lt;/p&gt;

&lt;p&gt;Copying a custom &lt;code&gt;Draft&lt;/code&gt; instance retained its ordinary &lt;code&gt;title&lt;/code&gt; data property. However, &lt;code&gt;instanceof Draft&lt;/code&gt; was &lt;code&gt;false&lt;/code&gt;, and the prototype's &lt;code&gt;label()&lt;/code&gt; method was absent. The JSON roundtrip produced the same observations.&lt;/p&gt;

&lt;p&gt;Copies made from &lt;code&gt;Object.freeze({ count: 1 })&lt;/code&gt; were not frozen under either method, and &lt;code&gt;count&lt;/code&gt; was writable. For an object with a getter, both methods invoked it while reading the value. The copy contained an ordinary property with the value &lt;code&gt;2&lt;/code&gt;. Copying once with each method invoked the getter a total of 2 times.&lt;/p&gt;

&lt;p&gt;These results support using class-specific construction when the goal includes reproducing a domain object's behavior. Copying this class did not produce another instance with the same callable methods.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transfer does not leave the original buffer available
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&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;Uint8Array&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="mi"&gt;2&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;moved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;structuredClone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&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;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;moved&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This printed &lt;code&gt;0 [ 1, 2, 3 ]&lt;/code&gt;. The destination retained the values, while the original ArrayBuffer was detached. If another operation still needs the source buffer, this cannot be treated as an ordinary copy. &lt;a href="https://nodejs.org/download/release/v24.15.0/docs/api/globals.html#structuredclonevalue-options" rel="noopener noreferrer"&gt;Node.js structuredClone&lt;/a&gt; provides the WHATWG API with this transfer option.&lt;/p&gt;

&lt;p&gt;The experiment measured differences in values, references, and exceptions. It did not compare speed, memory use for large inputs, or browser-specific objects such as DOM elements. A protocol that exchanges JSON still needs conversion to that format. This experiment helps identify which changes are acceptable when reusing a JSON roundtrip for an in-memory copy.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/language-batch08/run-runtime.mjs" rel="noopener noreferrer"&gt;execution script&lt;/a&gt; and &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-08/clone-results.json" rel="noopener noreferrer"&gt;results for the 12 cases and transfer&lt;/a&gt; record the shape of each copy as well as whether copying succeeded.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>testing</category>
    </item>
    <item>
      <title>What remains without import type? Comparing tsc, esbuild, and side effects</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Mon, 26 Oct 2026 01:58:59 +0000</pubDate>
      <link>https://dev.to/hirodeath/what-remains-without-import-type-comparing-tsc-esbuild-and-side-effects-3pmi</link>
      <guid>https://dev.to/hirodeath/what-remains-without-import-type-comparing-tsc-esbuild-and-side-effects-3pmi</guid>
      <description>&lt;p&gt;Some configurations automatically erase an import used only as a type, even without &lt;code&gt;import type&lt;/code&gt;. With &lt;code&gt;verbatimModuleSyntax: true&lt;/code&gt;, however, the spelling also changed whether a module's side effect ran. Check type-checking success separately from runtime module evaluation.&lt;/p&gt;

&lt;p&gt;In particular, these forms do not always emit the same JavaScript:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;import type { Ticket } from "./registry.js"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;import { type Ticket } from "./registry.js"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Put a visible marker in the imported module
&lt;/h2&gt;

&lt;p&gt;The experiment ran on September 11, 2026, with macOS arm64, Node.js 24.15.0, TypeScript 7.0.2, and esbuild 0.28.2. Compiler options were &lt;code&gt;strict: true&lt;/code&gt;, &lt;code&gt;target: ES2022&lt;/code&gt;, &lt;code&gt;module: ESNext&lt;/code&gt;, and &lt;code&gt;moduleResolution: Bundler&lt;/code&gt;. esbuild targeted Node ESM with bundling and tree shaking enabled. The test package did not declare &lt;code&gt;sideEffects: false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;registry.ts&lt;/code&gt; contains only a type and a log showing that the module was evaluated. It performs no external requests or real application initialization.&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Ticket&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;registry evaluated&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;The caller starts with this code:&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;Ticket&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./registry.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;ticket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ticket&lt;/span&gt; &lt;span class="o"&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="s2"&gt;Fix timer&lt;/span&gt;&lt;span class="dl"&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;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the import was changed when comparing emitted JavaScript and runtime logs. In the personal repository, the &lt;a href="https://github.com/takahiro-saeki/circle-hub/blob/770de5f2989775cfd95f7a9c4529565a2b48d2fd/packages/api/src/index.ts" rel="noopener noreferrer"&gt;type-sharing package&lt;/a&gt; exposes API types through &lt;code&gt;export type&lt;/code&gt;. The &lt;code&gt;Ticket&lt;/code&gt; and log here are a minimal example designed to separate type references from runtime loading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both spelling and configuration affected the result
&lt;/h2&gt;

&lt;p&gt;In this table, "evaluation" means the log printed &lt;code&gt;registry evaluated&lt;/code&gt;. Every successful example also printed the caller's &lt;code&gt;Fix timer&lt;/code&gt; message.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Import form&lt;/th&gt;
&lt;th&gt;verbatim&lt;/th&gt;
&lt;th&gt;tsc&lt;/th&gt;
&lt;th&gt;Registry evaluation in tsc output&lt;/th&gt;
&lt;th&gt;Registry evaluation in esbuild bundle&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;import { Ticket }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;import type { Ticket }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;import { type Ticket }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Side-effect import + &lt;code&gt;import type&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;import { Ticket }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;TS1484&lt;/td&gt;
&lt;td&gt;Emit stopped&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;import type { Ticket }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;import { type Ticket }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Side-effect import + &lt;code&gt;import type&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The test used &lt;code&gt;noEmitOnError: true&lt;/code&gt;, so no tsc-generated JavaScript was executed for the TS1484 row. esbuild bundled that input successfully, but that does not mean it passed type checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  An inline type modifier left an empty import
&lt;/h2&gt;

&lt;p&gt;This is the tsc output for &lt;code&gt;import { type Ticket }&lt;/code&gt; with &lt;code&gt;verbatimModuleSyntax: true&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./registry.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;ticket&lt;/span&gt; &lt;span class="o"&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="s2"&gt;Fix timer&lt;/span&gt;&lt;span class="dl"&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;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The name &lt;code&gt;Ticket&lt;/code&gt; disappeared, but &lt;code&gt;import {} from "./registry.js"&lt;/code&gt; remained. That dependency still evaluates the module, so the marker was printed. Making the entire statement type-only with &lt;code&gt;import type { Ticket }&lt;/code&gt; removed the statement and the log. The &lt;a href="https://www.typescriptlang.org/tsconfig/verbatimModuleSyntax.html" rel="noopener noreferrer"&gt;official verbatimModuleSyntax documentation&lt;/a&gt; also distinguishes these outputs.&lt;/p&gt;

&lt;p&gt;When only a type is needed, use a whole-statement &lt;code&gt;import type&lt;/code&gt;. When initialization is also required, make that a separate import:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./registry.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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Ticket&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./registry.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;ticket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ticket&lt;/span&gt; &lt;span class="o"&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="s2"&gt;Fix timer&lt;/span&gt;&lt;span class="dl"&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;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This printed the marker under both settings and both tools tested here. That does not establish the same outcome for dependencies marked as free of side effects or for other bundler configurations. This experiment observes two local files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Successful bundling does not establish type correctness
&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wrong&lt;/span&gt;&lt;span class="dl"&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;tsc reported TS2322, while esbuild succeeded and the bundle printed &lt;code&gt;string&lt;/code&gt;. The &lt;a href="https://esbuild.github.io/content-types/#typescript-caveats" rel="noopener noreferrer"&gt;esbuild TypeScript caveats&lt;/a&gt; explain that TypeScript's type checker is responsible for checking the annotations.&lt;/p&gt;

&lt;p&gt;Marking dependencies as type-only and running type checking in the build process address different concerns. In this reproduction, an input could run after import removal while still containing a mistake tsc detected. Bundle success alone did not catch that mistake.&lt;/p&gt;

&lt;p&gt;Run the T06 cases in the &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/language-batch08/compiler-cases.json" rel="noopener noreferrer"&gt;comparison inputs&lt;/a&gt; with the &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/language-batch08/run-compiler.mjs" rel="noopener noreferrer"&gt;execution script&lt;/a&gt; to inspect the combined &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-08/compiler-results.json" rel="noopener noreferrer"&gt;diagnostics, emitted JavaScript, and logs&lt;/a&gt;. When introducing the setting into an application, inspect type-only references and imports intended for initialization separately.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>esbuild</category>
      <category>testing</category>
    </item>
    <item>
      <title>Inspect emitted JavaScript before replacing enums with literal unions</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Sun, 25 Oct 2026 01:57:23 +0000</pubDate>
      <link>https://dev.to/hirodeath/inspect-emitted-javascript-before-replacing-enums-with-literal-unions-12f6</link>
      <guid>https://dev.to/hirodeath/inspect-emitted-javascript-before-replacing-enums-with-literal-unions-12f6</guid>
      <description>&lt;p&gt;If a state only needs to be constrained to &lt;code&gt;"ready"&lt;/code&gt; or &lt;code&gt;"waiting"&lt;/code&gt; in the type system, a literal union can express that. If code must also refer to a named value such as &lt;code&gt;State.Ready&lt;/code&gt; at runtime, it needs a value object as well.&lt;/p&gt;

&lt;p&gt;Separate those requirements when comparing an &lt;code&gt;enum&lt;/code&gt;, a literal union, and an &lt;code&gt;as const&lt;/code&gt; object. A regular enum emits JavaScript; it is more than another spelling for a type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Emit the same states in four forms
&lt;/h2&gt;

&lt;p&gt;The experiment ran on September 11, 2026, with macOS arm64, Node.js 24.15.0, and TypeScript 7.0.2. The build used &lt;code&gt;strict: true&lt;/code&gt;, &lt;code&gt;target: ES2022&lt;/code&gt;, &lt;code&gt;module: ESNext&lt;/code&gt;, and &lt;code&gt;moduleResolution: Bundler&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="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;NumericState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Waiting&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;StringState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Ready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Waiting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting&lt;/span&gt;&lt;span class="dl"&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;StateValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Waiting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kd"&gt;as const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NumericState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StringState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StateValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selected&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The log contained the following values. The emitted JavaScript was inspected as well.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;th&gt;What remained at runtime&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Numeric enum&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Ready: 0&lt;/code&gt; and &lt;code&gt;Waiting: 1&lt;/code&gt;, plus reverse mappings &lt;code&gt;0: "Ready"&lt;/code&gt; and &lt;code&gt;1: "Waiting"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String enum&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Ready: "ready"&lt;/code&gt; and &lt;code&gt;Waiting: "waiting"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Literal union &lt;code&gt;State&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The type disappeared; the value &lt;code&gt;"ready"&lt;/code&gt; assigned to &lt;code&gt;selected&lt;/code&gt; remained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;as const&lt;/code&gt; object&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Ready: "ready"&lt;/code&gt; and &lt;code&gt;Waiting: "waiting"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The numeric enum's reverse mapping, and its absence from the string enum, match the &lt;a href="https://www.typescriptlang.org/docs/handbook/enums.html#reverse-mappings" rel="noopener noreferrer"&gt;official enums reference&lt;/a&gt;. Reverse lookup is a runtime capability of the numeric enum. If the application does not use it, it need not be a selection requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Derive the value union from an object
&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;StateValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Waiting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kd"&gt;as const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;StateValue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;StateValue&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;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&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;value&lt;/span&gt;&lt;span class="p"&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;isFrozen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;StateValue&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;StateValue&lt;/code&gt; holds the named values, and the object supplies the allowed value type. Assigning &lt;code&gt;"ready"&lt;/code&gt; to &lt;code&gt;State&lt;/code&gt; passed, and the log printed &lt;code&gt;ready false&lt;/code&gt;. &lt;code&gt;Object.isFrozen&lt;/code&gt; was &lt;code&gt;false&lt;/code&gt; because &lt;code&gt;as const&lt;/code&gt; does not freeze objects at runtime.&lt;/p&gt;

&lt;p&gt;The string enum did not accept a direct assignment merely because the string matched a member's value:&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;enum&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Ready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Waiting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&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;State&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produced TS2322. A string received externally cannot simply be treated as an enum value. A literal union does not establish that an arbitrary, unchecked string is safe either; external input still needs runtime validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Whether const enum disappears also depends on configuration
&lt;/h2&gt;

&lt;p&gt;The following input was compiled with only &lt;code&gt;isolatedModules&lt;/code&gt; changed:&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="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Ready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Waiting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="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;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Ready&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;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;isolatedModules: false&lt;/code&gt;, the value was inlined in the emitted JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="cm"&gt;/* State.Ready */&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;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;isolatedModules: true&lt;/code&gt;, code constructing the &lt;code&gt;State&lt;/code&gt; object and accessing &lt;code&gt;State.Ready&lt;/code&gt; remained. Both versions printed &lt;code&gt;ready&lt;/code&gt;. Do not expect every build pipeline to erase the object just because the declaration says &lt;code&gt;const enum&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This comparison defines and uses the enum in the same source file. It does not test ambient const enums in a published library's &lt;code&gt;.d.ts&lt;/code&gt; files or mismatched versions between a definition and its consumers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Type stripping makes the syntax constraint visible
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;erasableSyntaxOnly: true&lt;/code&gt;, both the regular enum and const enum produced TS1294. The literal union and &lt;code&gt;as const&lt;/code&gt; object example passed. This option avoids syntax that needs transformation into JavaScript. The &lt;a href="https://www.typescriptlang.org/tsconfig/erasableSyntaxOnly.html" rel="noopener noreferrer"&gt;official configuration reference&lt;/a&gt; lists the affected constructs.&lt;/p&gt;

&lt;p&gt;Directly running a &lt;code&gt;.ts&lt;/code&gt; file containing the regular enum on Node.js 24.15.0, without transformation options, also produced &lt;code&gt;ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX&lt;/code&gt;. &lt;a href="https://nodejs.org/download/release/v24.15.0/docs/api/typescript.html" rel="noopener noreferrer"&gt;Node.js TypeScript support&lt;/a&gt; distinguishes execution through type stripping from features that require syntax transformation.&lt;/p&gt;

&lt;p&gt;A literal union is a candidate when only a type is needed. Deriving a union from an object also provides named runtime values. If existing code depends on enum reverse lookup or a public enum API, inspect those consumers before changing it. This output comparison does not establish a bundle-size or execution-speed advantage.&lt;/p&gt;

&lt;p&gt;The T05 cases in the &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/language-batch08/compiler-cases.json" rel="noopener noreferrer"&gt;comparison inputs&lt;/a&gt; and the &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-08/compiler-results.json" rel="noopener noreferrer"&gt;compiler and runtime results&lt;/a&gt; preserve the details. Compare alternatives under the runtime requirements and transformation settings the application will actually use.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>node</category>
    </item>
    <item>
      <title>Claim guest responses by transferring control of a record, not matching a name</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Sat, 24 Oct 2026 02:11:29 +0000</pubDate>
      <link>https://dev.to/hirodeath/claim-guest-responses-by-transferring-control-of-a-record-not-matching-a-name-k1e</link>
      <guid>https://dev.to/hirodeath/claim-guest-responses-by-transferring-control-of-a-record-not-matching-a-name-k1e</guid>
      <description>&lt;p&gt;Someone answers an event invitation without logging in and later wants to change the response. Using their name as proof of identity cannot distinguish namesakes or a different person logging in on a shared device.&lt;/p&gt;

&lt;p&gt;SquadNote issues a token controlling one response instead of creating a placeholder user row. Later login does not automatically match names: the person reviews the target responses and explicitly claims them for the account.&lt;/p&gt;

&lt;p&gt;A response token grants control of a record; identifying a person requires separate evidence. This investigation read the design, migration, API, and device-storage code at &lt;code&gt;circle-hub&lt;/code&gt; commit &lt;code&gt;9aec489&lt;/code&gt;, then reran related tests on September 11, 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add response ownership before building an organization roster
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/circle-hub/blob/9aec48963842dcaf16ce49c80317366509ab1b8f/docs/growth/09-guest-response-ownership.md" rel="noopener noreferrer"&gt;design document&lt;/a&gt; separates adding control to existing &lt;code&gt;guest_attendance&lt;/code&gt; records from a future organization-level provisional membership feature.&lt;/p&gt;

&lt;p&gt;The implemented stage is the former. Ownership belongs to an event response, supporting listing, editing, and account claiming without treating a matching name as proof of identity.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/circle-hub/blob/9aec48963842dcaf16ce49c80317366509ab1b8f/apps/web/migrations/0007_add_guest_response_ownership.sql" rel="noopener noreferrer"&gt;migration SQL&lt;/a&gt; adds these columns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ownership_token_hash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hash used to verify unauthenticated control of the response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claimed_by_user_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Account that claimed it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claimed_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claim time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;attending_since&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Time the response became attending&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;updated_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Existing responses do not receive ownership tokens retroactively. Matching a name does not grant control over past responses. How preexisting rows participate in a new feature is part of the migration contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the response ID from its operation token
&lt;/h2&gt;

&lt;p&gt;The response ID locates the target. It does not authorize editing by itself; the token returned when the response was created must also be verified.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/circle-hub/blob/9aec48963842dcaf16ce49c80317366509ab1b8f/apps/web/src/server/api/lib/guest-response-ownership.ts" rel="noopener noreferrer"&gt;token implementation&lt;/a&gt; generates 32 bytes with &lt;code&gt;crypto.getRandomValues()&lt;/code&gt; and represents them as 43 base64url characters. Only the SHA-256 hash enters the database. The &lt;a href="https://www.w3.org/TR/webcrypto/#Crypto-method-getRandomValues" rel="noopener noreferrer"&gt;Web Crypto specification&lt;/a&gt; defines that method as a source of cryptographically strong random values.&lt;/p&gt;

&lt;p&gt;This creates a string granting control. It does not prove that its holder is the person named in the response. The model permits whoever holds a valid token to operate on that record.&lt;/p&gt;

&lt;p&gt;Keeping plaintext out of the database prevents simply replaying the stored hash as the operation key. It does not remove the need to protect the original token on the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Losing storage can remove control without deleting the response
&lt;/h2&gt;

&lt;p&gt;The implementation stores response IDs and tokens in Web &lt;code&gt;localStorage&lt;/code&gt; and mobile &lt;code&gt;SecureStore&lt;/code&gt;. Tokens are kept out of URLs; the necessary references are sent to listing and mutation APIs.&lt;/p&gt;

&lt;p&gt;Web storage parsing discards malformed rows and returns an empty array if reading fails. That prevents a crash. It does not establish that no database responses exist or recover the person's responses.&lt;/p&gt;

&lt;p&gt;The saved-reference limit is 100. Clearing storage, opening another device, or dropping an old reference beyond that limit has no implemented name-based recovery path. Automatic cross-device synchronization is also outside this stage.&lt;/p&gt;

&lt;p&gt;As &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html#local-storage" rel="noopener noreferrer"&gt;OWASP's localStorage guidance&lt;/a&gt; explains, JavaScript-readable storage is exposed to XSS. Placing these values in localStorage does not make them secure by itself. This investigation did not audit the application's complete XSS defenses or storage on physical devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claim after login and confirmation, then remove the old key
&lt;/h2&gt;

&lt;p&gt;After login, the screen displays locally saved responses and calls the claim API only through an explicit action. Logging another account into a shared device does not automatically attach those responses to it.&lt;/p&gt;

&lt;p&gt;After checking the authenticated account and token, the &lt;a href="https://github.com/takahiro-saeki/circle-hub/blob/9aec48963842dcaf16ce49c80317366509ab1b8f/apps/web/src/server/api/routers/attendance.ts" rel="noopener noreferrer"&gt;claim API&lt;/a&gt; updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;claimedByUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;claimedAt&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;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;ownershipTokenHash&lt;/span&gt;&lt;span class="p"&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;This is the update object excerpted from the implementation. Surrounding code checks the session, existing claimant, and token. The update condition also retains the response ID, an unset claimant, and the original token hash. Filtering is therefore present at the write as well as during the preceding check.&lt;/p&gt;

&lt;p&gt;Only successful IDs are removed from device storage. Subsequent access uses the logged-in user's ID. Rejecting reassignment to another account and clearing the old token hash define the transfer of control.&lt;/p&gt;

&lt;p&gt;The API processes multiple responses sequentially and returns &lt;code&gt;claimedIds&lt;/code&gt; and &lt;code&gt;rejectedIds&lt;/code&gt;. The client must not treat the whole list as successful and delete every reference. Separate results preserve which records actually transferred when some were rejected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 15 rerun tests covered
&lt;/h2&gt;

&lt;p&gt;All 15 tests in 3 related files passed from the fixed commit. The run used Node.js &lt;code&gt;v24.15.0&lt;/code&gt; and Vitest &lt;code&gt;4.1.4&lt;/code&gt; in a temporary copy, without changing the original working directory.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Checked behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Token functions&lt;/td&gt;
&lt;td&gt;Encoding, different generated values, correct hash verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response API&lt;/td&gt;
&lt;td&gt;Wrong-token rejection, valid references, ownership data omitted from public output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visibility&lt;/td&gt;
&lt;td&gt;Responses for an event made private are omitted from the locally saved list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claiming&lt;/td&gt;
&lt;td&gt;Reassignment rejection, hash clearing, and the branch that does not report an update as successful after a conflict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web storage&lt;/td&gt;
&lt;td&gt;Malformed data, replacement of the same response ID, removal of successful IDs only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The token functions use the actual randomness and hashing APIs. API tests invoke the actual router but mock the database and notifications. They do not reproduce SQL predicate evaluation or multiprocess races in real D1.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/batch05-circle-tests.py" rel="noopener noreferrer"&gt;runner&lt;/a&gt; and &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-05/guest-tests.json" rel="noopener noreferrer"&gt;results&lt;/a&gt; preserve that scope. This run did not exercise SecureStore on physical mobile devices or operate the claiming screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not infer personal identity from control of a response
&lt;/h2&gt;

&lt;p&gt;Implemented behavior consists of record-level tokens, management from the same device, and explicit transfer to an account. A provisional organization roster, automatic retrieval of past responses by name, and cross-device synchronization remain separate future features.&lt;/p&gt;

&lt;p&gt;Before expanding guest convenience, specify what possession of each credential permits. Keeping control of one response from implicitly granting control of namesakes' responses or roster entries makes the next identity-verification requirement clearer.&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>architecture</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Choosing type or interface by declaration merging and conflict diagnostics</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Fri, 23 Oct 2026 02:12:35 +0000</pubDate>
      <link>https://dev.to/hirodeath/choosing-type-or-interface-by-declaration-merging-and-conflict-diagnostics-2ob8</link>
      <guid>https://dev.to/hirodeath/choosing-type-or-interface-by-declaration-merging-and-conflict-diagnostics-2ob8</guid>
      <description>&lt;p&gt;A useful starting point for choosing &lt;code&gt;type&lt;/code&gt; or &lt;code&gt;interface&lt;/code&gt; is whether other declarations should be allowed to extend the same named contract. Both can describe ordinary objects, but adding declarations and combining conflicting properties produce different diagnostics.&lt;/p&gt;

&lt;p&gt;Compiling small examples with TypeScript 7.0.2 gave these results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add a different property through another declaration of the same &lt;code&gt;interface&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Declarations merge; the added property is also required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redeclare the same &lt;code&gt;type&lt;/code&gt; name&lt;/td&gt;
&lt;td&gt;TS2300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change &lt;code&gt;id: string&lt;/code&gt; to &lt;code&gt;id: number&lt;/code&gt; through &lt;code&gt;interface extends&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;TS2430 at the extending declaration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Define &lt;code&gt;{ id: string } &amp;amp; { id: number }&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Definition passes; &lt;code&gt;id&lt;/code&gt; becomes &lt;code&gt;never&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assign &lt;code&gt;{ id: 1 }&lt;/code&gt; to that intersection&lt;/td&gt;
&lt;td&gt;TS2322 at the assignment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Merging an interface also affects existing consumers
&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;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Ticket&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;title&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="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Ticket&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;traceId&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ticket&lt;/span&gt; &lt;span class="o"&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="s2"&gt;Fix timer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local-1&lt;/span&gt;&lt;span class="dl"&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;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles, runs, and prints &lt;code&gt;local-1&lt;/code&gt;. The second declaration adds a member to the same &lt;code&gt;Ticket&lt;/code&gt;; it does not create a derived type with a different name. An object that omits &lt;code&gt;traceId&lt;/code&gt; now produces TS2741.&lt;/p&gt;

&lt;p&gt;The official &lt;a href="https://www.typescriptlang.org/docs/handbook/declaration-merging.html" rel="noopener noreferrer"&gt;declaration merging documentation&lt;/a&gt; describes how interface members merge and the matching-type requirement when a non-function member is declared again. This experiment covers the case of adding a different property.&lt;/p&gt;

&lt;p&gt;A library can use this behavior as an extension contract for consumers. Adding a required member, however, also changes type checking for existing code. This does not mean identical names in arbitrary files automatically merge across an entire project. The declarations must resolve to the same target in the relevant scope. Real library extensions may also require mechanisms such as module augmentation.&lt;/p&gt;

&lt;p&gt;This is a composition experiment within one module, not an implemented extension for a particular library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing redeclaration is different from restricting values exactly
&lt;/h2&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;Ticket&lt;/span&gt; &lt;span class="o"&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="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;Ticket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;traceId&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="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Declaring both aliases in the same module produces TS2300 on both declarations. That does not mean using &lt;code&gt;type&lt;/code&gt; always rejects extra object properties.&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;Ticket&lt;/span&gt; &lt;span class="o"&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="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;input&lt;/span&gt; &lt;span class="o"&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="s2"&gt;Fix timer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;debug&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ticket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;debug&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This passed and printed &lt;code&gt;true&lt;/code&gt;. The annotation did not remove &lt;code&gt;debug&lt;/code&gt; at runtime. Whether an alias can be reopened for declaration merging is separate from whether a value is structurally assignable. &lt;a href="https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces" rel="noopener noreferrer"&gt;Everyday types&lt;/a&gt; covers the shared capabilities of aliases and interfaces and their difference in declaration merging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check where a composition conflict fails
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TextId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;NumericId&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;TextId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fails at the declaration of &lt;code&gt;NumericId&lt;/code&gt;. Replacing &lt;code&gt;id&lt;/code&gt; with a number means it no longer satisfies &lt;code&gt;TextId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An intersection instead requires both conditions at once:&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;TextId&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Combined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TextId&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;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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;IsNever&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;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;true&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IsNever&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Combined&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;check&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The definition itself passed. A small conditional type confirmed that &lt;code&gt;Combined["id"]&lt;/code&gt; was &lt;code&gt;never&lt;/code&gt;, and assigning &lt;code&gt;true&lt;/code&gt; to &lt;code&gt;check&lt;/code&gt; also passed. The property would need to satisfy both &lt;code&gt;string&lt;/code&gt; and &lt;code&gt;number&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Assigning &lt;code&gt;{ id: 1 }&lt;/code&gt; to this &lt;code&gt;Combined&lt;/code&gt; then produced TS2322. The &lt;code&gt;&amp;amp;&lt;/code&gt; operator does not overwrite a shared property with the type on its right. A combined type can be declared even when it has no usable value.&lt;/p&gt;

&lt;h2&gt;
  
  
  What these results justify
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;interface&lt;/code&gt; is a candidate when declaration merging is an intentional extension point for consumers. Use &lt;code&gt;type&lt;/code&gt; to name alternatives such as a union. For application object types that either form can express, following the existing code convention is reasonable. This experiment did not establish a reason to rewrite all such declarations into one form.&lt;/p&gt;

&lt;p&gt;The tests ran on September 11, 2026, with macOS arm64, Node.js 24.15.0, TypeScript 7.0.2, &lt;code&gt;strict: true&lt;/code&gt;, &lt;code&gt;target: ES2022&lt;/code&gt;, &lt;code&gt;module: ESNext&lt;/code&gt;, and &lt;code&gt;moduleResolution: Bundler&lt;/code&gt;. Type declarations disappeared from the emitted JavaScript; the object and logging code remained. Compiler performance and large module-augmentation setups were not measured.&lt;/p&gt;

&lt;p&gt;The T04 cases in the &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/language-batch08/compiler-cases.json" rel="noopener noreferrer"&gt;reproduction code&lt;/a&gt; and the &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-08/compiler-results.json" rel="noopener noreferrer"&gt;diagnostics and emitted files&lt;/a&gt; show whether each error occurs at declaration or assignment.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>testing</category>
    </item>
    <item>
      <title>Comparing satisfies, type annotations, and assertions through compiler output</title>
      <dc:creator>hiro@</dc:creator>
      <pubDate>Thu, 22 Oct 2026 02:07:14 +0000</pubDate>
      <link>https://dev.to/hirodeath/comparing-satisfies-type-annotations-and-assertions-through-compiler-output-4f50</link>
      <guid>https://dev.to/hirodeath/comparing-satisfies-type-annotations-and-assertions-through-compiler-output-4f50</guid>
      <description>&lt;p&gt;Use &lt;code&gt;satisfies&lt;/code&gt; when a configuration value should meet a type while its individual properties remain convenient to use. Two common shortcuts are misleading, though: that it can never affect inference, and that it always rejects extra keys.&lt;/p&gt;

&lt;p&gt;The comparison uses the same object under a type annotation, &lt;code&gt;satisfies&lt;/code&gt;, and &lt;code&gt;as&lt;/code&gt;, with emitted declarations and diagnostics as evidence. For this display configuration, the differences were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Form&lt;/th&gt;
&lt;th&gt;Type when reading &lt;code&gt;ready&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Missing &lt;code&gt;waiting&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;: Notices&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;`string&lt;/td&gt;
&lt;td&gt;number[]`&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;satisfies Notices&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;as Notices&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;`string&lt;/td&gt;
&lt;td&gt;number[]`&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Inspect declarations as well as editor hints
&lt;/h2&gt;

&lt;p&gt;The experiment ran on September 11, 2026, with macOS arm64, Node.js 24.15.0, and TypeScript 7.0.2. The compiler used &lt;code&gt;strict: true&lt;/code&gt;, &lt;code&gt;target: ES2022&lt;/code&gt;, &lt;code&gt;module: ESNext&lt;/code&gt;, and &lt;code&gt;moduleResolution: Bundler&lt;/code&gt;, with type checking and declaration emit. The display values are synthetic test data.&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;Notices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&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="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;annotated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Notices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;waiting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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;checked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;waiting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;Notices&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;asserted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;waiting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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;Notices&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;checked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;checked.ready.toUpperCase()&lt;/code&gt; passed and printed &lt;code&gt;READY&lt;/code&gt;. Calling the same method on &lt;code&gt;annotated.ready&lt;/code&gt; produced TS2339 because the property could also be an array.&lt;/p&gt;

&lt;p&gt;The emitted declarations explain that difference:&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;Notices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&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="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&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;annotated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Notices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&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;checked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;ready&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;waiting&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="k"&gt;export&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;asserted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Notices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&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;checked&lt;/code&gt; retains the types of individual properties. The other two variables have the annotated &lt;code&gt;Notices&lt;/code&gt; type. This makes &lt;code&gt;satisfies&lt;/code&gt; useful when a configuration needs a shared compatibility check without requiring extra narrowing wherever a particular value is used. The &lt;a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html" rel="noopener noreferrer"&gt;TypeScript 4.9 documentation&lt;/a&gt; describes this use of compatibility checks while retaining a useful expression type.&lt;/p&gt;

&lt;h2&gt;
  
  
  An assertion can leave a missing property undetected
&lt;/h2&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;Notices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&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="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Notices&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;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;waiting&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code passed type checking and printed &lt;code&gt;undefined&lt;/code&gt;. Changing the assertion to &lt;code&gt;satisfies Notices&lt;/code&gt; produced TS2741 for the missing &lt;code&gt;waiting&lt;/code&gt; property. An assertion does not create that property.&lt;/p&gt;

&lt;p&gt;This also does not mean every possible assertion is accepted. TypeScript can reject an assertion based on the relationship between the types. The finding here is narrower: it accepts this particular object despite the missing required key. As the &lt;a href="https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions" rel="noopener noreferrer"&gt;official explanation of type assertions&lt;/a&gt; states, assertions do not validate or convert runtime values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context can affect inference
&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&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;count&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&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;count&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="nx"&gt;satisfies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;count&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frozenType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&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;count&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="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the emitted declarations, &lt;code&gt;plain.enabled&lt;/code&gt; was &lt;code&gt;boolean&lt;/code&gt;, while &lt;code&gt;checked.enabled&lt;/code&gt; was &lt;code&gt;true&lt;/code&gt;. Both &lt;code&gt;count&lt;/code&gt; properties were &lt;code&gt;number&lt;/code&gt;. Context from the target type means literal inference is not necessarily identical.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;as const&lt;/code&gt;, &lt;code&gt;frozenType.enabled&lt;/code&gt; became a readonly &lt;code&gt;true&lt;/code&gt;, and &lt;code&gt;frozenType.count&lt;/code&gt; became a readonly &lt;code&gt;1&lt;/code&gt;. This does not freeze the object at runtime either. If a configuration will be modified later, decide whether that property should remain the literal &lt;code&gt;true&lt;/code&gt; or accept both boolean values, including &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extra keys are not always rejected
&lt;/h2&gt;

&lt;p&gt;Writing &lt;code&gt;{ ready: "Ready", waiting: [1], typo: 1 } satisfies Notices&lt;/code&gt; directly produced TS2353 for the extra &lt;code&gt;typo&lt;/code&gt; property. Assigning that object to a variable first changed the outcome:&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;Notices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&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="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;waiting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;typo&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;Notices&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;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;typo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This passed and printed &lt;code&gt;1&lt;/code&gt;. A value can satisfy the required structure while retaining additional properties. Restricting the keys of external data requires a runtime check separate from these type constructs.&lt;/p&gt;

&lt;p&gt;For the local configuration in this experiment, use &lt;code&gt;satisfies&lt;/code&gt; to catch mistakes while retaining operations specific to each value. Use an annotation to specify the type the variable should expose. Reserve &lt;code&gt;as&lt;/code&gt; for places where other evidence supports the asserted type.&lt;/p&gt;

&lt;p&gt;The T02 cases in the &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/language-batch08/compiler-cases.json" rel="noopener noreferrer"&gt;reproduction code&lt;/a&gt; and the &lt;a href="https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-08/compiler-results.json" rel="noopener noreferrer"&gt;execution record&lt;/a&gt; include both accepted declarations and deliberately invalid inputs. Do not assume diagnostic numbers or inference remain identical across compiler versions; compare declaration output when upgrading.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
