DEV Community: YAMAMOTO Yuji The latest articles on DEV Community by YAMAMOTO Yuji (@igrep). https://dev.to/igrep https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F149436%2F1b1db0ff-b11f-4880-a7d0-6fd8db2fce54.png DEV Community: YAMAMOTO Yuji https://dev.to/igrep en Flexiblly Extend Nested Structures - "Trees that Grow" in TypeScript YAMAMOTO Yuji Mon, 20 Mar 2023 05:08:42 +0000 https://dev.to/igrep/flexiblly-extend-nested-structures-trees-that-grow-in-typescript-4347 https://dev.to/igrep/flexiblly-extend-nested-structures-trees-that-grow-in-typescript-4347 <p>Among Haskell developers familiar with ASTs, an idiom called <a href="proxy.php?url=https://www.microsoft.com/en-us/research/uploads/prod/2016/11/trees-that-grow.pdf">Trees that Grow</a> is so common that their most popular compiler GHC adopts it. Today, I'll share you with how to implement the idea in TypeScript.</p> <h2> Summary </h2> <p>In Haskell, the "Tree-Decoration Problem" is solved by combining open type families with the base nested type, while TypeScript doesn't support open type families (at least no straightforward approach). But I found that open type families are not necessary because the actual key to solving the "Tree-Decoration Problem" is packing up type parameters into one structure. TypeScript supports it with Indexed Access Types, but as far as I know, Haskell doesn't.</p> <p>Some important part of the example code in this article is available with running examples in <a href="proxy.php?url=https://github.com/igrep/ts-that-grow">https://github.com/igrep/ts-that-grow</a>. And you can run it directly in <a href="proxy.php?url=https://codesandbox.io/p/github/igrep/ts-that-grow/main?file=%2Fsrc%2Fextensible.ts&amp;selection=%5B%7B%22endColumn%22%3A65%2C%22endLineNumber%22%3A27%2C%22startColumn%22%3A65%2C%22startLineNumber%22%3A27%7D%5D&amp;workspace=%257B%2522activeFileId%2522%253A%2522clfas23kh0005g8hu2mhs5l2o%2522%252C%2522openFiles%2522%253A%255B%2522%252FREADME.md%2522%255D%252C%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522gitSidebarPanel%2522%253A%2522COMMIT%2522%252C%2522spaces%2522%253A%257B%2522clfas1iwu000x2a6er8dbezte%2522%253A%257B%2522key%2522%253A%2522clfas1iwu000x2a6er8dbezte%2522%252C%2522name%2522%253A%2522build%2522%252C%2522devtools%2522%253A%255B%255D%257D%252C%2522clfas4mvi00sd2a6e2iu96lh8%2522%253A%257B%2522key%2522%253A%2522clfas4mvi00sd2a6e2iu96lh8%2522%252C%2522devtools%2522%253A%255B%255D%252C%2522name%2522%253A%2522Terminal%2522%257D%257D%252C%2522currentSpace%2522%253A%2522clfas1iwu000x2a6er8dbezte%2522%252C%2522spacesOrder%2522%253A%255B%2522clfas1iwu000x2a6er8dbezte%2522%252C%2522clfas4mvi00sd2a6e2iu96lh8%2522%255D%252C%2522hideCodeEditor%2522%253Afalse%257D">CodeSandbox</a>.</p> <h2> Problem </h2> <p>Compilers written in a statically-typed language usually have their AST (abstract syntax tree) type somewhere in the source code to represent the target language's syntax. Most AST types are defined recursively, which makes them hard to extend by adding some additional information to each type of node.</p> <p>As an example used everywhere in this article, let me define a simple AST type:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="c1">// An AST type for a fictitious language that supports</span> <span class="c1">// only numeric literals, variables, assignment to a variable,</span> <span class="c1">// function expressions, and calling a function.</span> <span class="kd">type</span> <span class="nx">Expression</span> <span class="o">=</span> <span class="nx">Literal</span> <span class="o">|</span> <span class="nx">Variable</span> <span class="o">|</span> <span class="nx">SetVariable</span> <span class="o">|</span> <span class="nx">Func</span> <span class="o">|</span> <span class="nx">CallFunc</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">Literal</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Literal</span><span class="dl">"</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">Variable</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Variable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">SetVariable</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">SetVariable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">Func</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Func</span><span class="dl">"</span><span class="p">;</span> <span class="nl">argumentName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">body</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">CallFunc</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">CallFunc</span><span class="dl">"</span><span class="p">;</span> <span class="nl">function</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">;</span> <span class="nl">argument</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">;</span> <span class="p">};</span> </code></pre> </div> <p>This is the basic type containing just the essential information of each node. We'll fill it with richer data for different use cases.</p> <p>Say, let's add the location where each node is found in the source file:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">Expression</span> <span class="o">=</span> <span class="nx">Literal</span> <span class="o">|</span> <span class="nx">Variable</span> <span class="o">|</span> <span class="nx">SetVariable</span> <span class="o">|</span> <span class="nx">Func</span> <span class="o">|</span> <span class="nx">CallFunc</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">Location</span> <span class="o">=</span> <span class="p">{</span> <span class="na">row</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">column</span><span class="p">:</span> <span class="kr">number</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">Literal</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Literal</span><span class="dl">"</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">location</span><span class="p">:</span> <span class="nx">Location</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">Variable</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Variable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">location</span><span class="p">:</span> <span class="nx">Location</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">SetVariable</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">SetVariable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">;</span> <span class="nl">location</span><span class="p">:</span> <span class="nx">Location</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">Func</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Func</span><span class="dl">"</span><span class="p">;</span> <span class="nl">argumentName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">body</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">;</span> <span class="nl">location</span><span class="p">:</span> <span class="nx">Location</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">CallFunc</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">CallFunc</span><span class="dl">"</span><span class="p">;</span> <span class="nl">function</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">;</span> <span class="nl">argument</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">;</span> <span class="nl">location</span><span class="p">:</span> <span class="nx">Location</span><span class="p">;</span> <span class="p">};</span> </code></pre> </div> <p>We could also, qualify the variable names after resolving where every variable comes from:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">Expression</span> <span class="o">=</span> <span class="nx">Literal</span> <span class="o">|</span> <span class="nx">Variable</span> <span class="o">|</span> <span class="nx">SetVariable</span> <span class="o">|</span> <span class="nx">Func</span> <span class="o">|</span> <span class="nx">CallFunc</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">Location</span> <span class="o">=</span> <span class="p">{</span> <span class="na">row</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">column</span><span class="p">:</span> <span class="kr">number</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">Literal</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Literal</span><span class="dl">"</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">location</span><span class="p">:</span> <span class="nx">Location</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">Variable</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Variable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">location</span><span class="p">:</span> <span class="nx">Location</span><span class="p">;</span> <span class="nl">namespace</span><span class="p">:</span> <span class="p">{</span> <span class="na">qualifier</span><span class="p">:</span> <span class="kr">string</span> <span class="p">}</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">SetVariable</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">SetVariable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">;</span> <span class="nl">location</span><span class="p">:</span> <span class="nx">Location</span><span class="p">;</span> <span class="nl">namespace</span><span class="p">:</span> <span class="p">{</span> <span class="na">qualifier</span><span class="p">:</span> <span class="kr">string</span> <span class="p">}</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">Func</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Func</span><span class="dl">"</span><span class="p">;</span> <span class="nl">argumentName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">body</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">;</span> <span class="nl">location</span><span class="p">:</span> <span class="nx">Location</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">CallFunc</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">CallFunc</span><span class="dl">"</span><span class="p">;</span> <span class="nl">function</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">;</span> <span class="nl">argument</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">;</span> <span class="nl">location</span><span class="p">:</span> <span class="nx">Location</span><span class="p">;</span> <span class="p">};</span> </code></pre> </div> <p>And we might want to add even more!</p> <p>But here is a problem: not all properties are always necessary or available. Thus adding an extension to the single type would result in an overload. We should split the types for separation of concerns.</p> <p>Of course, we often see this kind of problem for any complex types we define for daily life as a programmer. In such a case, we may intuitively think of a solution that is just wrapping the top-level of the <code>Expression</code> type:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">Expression</span> <span class="o">=</span> <span class="nx">Literal</span> <span class="o">|</span> <span class="nx">Variable</span> <span class="o">|</span> <span class="nx">SetVariable</span> <span class="o">|</span> <span class="nx">Func</span> <span class="o">|</span> <span class="nx">CallFunc</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">Location</span> <span class="o">=</span> <span class="p">{</span> <span class="na">row</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">column</span><span class="p">:</span> <span class="kr">number</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">ExpressionWithLocation</span> <span class="o">=</span> <span class="nx">Expression</span> <span class="o">&amp;</span> <span class="nx">Location</span><span class="p">;</span> <span class="p">...</span> </code></pre> </div> <p>This does not work when the type is recursive. For example in <code>Expression</code>, several kinds of nodes (namely <code>SetVariable</code>, <code>Func</code>, and <code>CallFunc</code>) contain <code>Expression</code> itself. Hence the <code>&amp; Location</code> of <code>ExpressionWithLocation</code> is not applied to the <code>Expression</code> inside such nodes. This is the "Tree-Decoration Problem".</p> <h3> Incomplete Solution 1: Parameterize an Extension to Pass Around </h3> <p>You might invent one thing after reading the last example: "How about parameterizing <code>Location</code>?":<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">=</span> <span class="nx">Literal</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">Variable</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">SetVariable</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">Func</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">CallFunc</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">Location</span> <span class="o">=</span> <span class="p">{</span> <span class="na">row</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">column</span><span class="p">:</span> <span class="kr">number</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">ExpressionWithLocation</span> <span class="o">=</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Location</span><span class="o">&gt;</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">Literal</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Literal</span><span class="dl">"</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">Extension</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">Variable</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Variable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">Extension</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">SetVariable</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">SetVariable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">Extension</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">Func</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Func</span><span class="dl">"</span><span class="p">;</span> <span class="nl">argumentName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">body</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">Extension</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">CallFunc</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">CallFunc</span><span class="dl">"</span><span class="p">;</span> <span class="nl">function</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span><span class="p">;</span> <span class="nl">argument</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">Extension</span><span class="p">;</span> </code></pre> </div> <p><code>&amp; Extension</code> is now applied to every type of node by replacing all of the <code>Expression</code>s with <code>Expression&lt;Extension&gt;</code>, including ones in <code>SetVariable</code>, <code>Func</code>, and <code>CallFunc</code>.</p> <p>Yes, this does perfectly with <code>Location</code>, and sometimes fairly enough to avoid extra complexity. But this approach doesn't allow us to make the <code>ExpressionWithNamespace</code> type: <code>namespace</code> should be added only to the <code>Variable</code> and <code>SetVariable</code> nodes.</p> <p>To generalize the problem, if we have extensions that add fields that differ by node type, parameterizing with a single type variable isn't sufficient.</p> <p>According to <a href="proxy.php?url=https://www.microsoft.com/en-us/research/uploads/prod/2016/11/trees-that-grow.pdf">the paper I referred to at the beginning of this article</a>, the GHC developers were faced with the same problem. GHC's AST type <code>HsSyn</code> is used across various compiler phases with different addition to the core AST, and some external libraries also want to reuse the <code>HsSyn</code> with their customization. Before inventing "Trees that Grow", they had to make a compromise with some imperfect solutions such as creating independent AST types.</p> <h2> Solutions </h2> <h3> Follow the Original Idea: Use Type Families </h3> <p>The "Trees that Grow" paper overcame the "Tree-Decoration Problem" with open type families. Unfortunately, TypeScript doesn't support <em>open</em> type families. <a href="proxy.php?url=https://www.javiercasas.com/articles/typescript-type-families">The first page I found by googling with "typescript type family"</a> introduces TypeScript's type families, but they are only <em>closed</em> type families. "Open" type families and "closed" type families are different in that the former enables us to append a condition and the result type afterward. Let's grab the idea by imagining if TypeScript had the open type families feature to compare them.</p> <p>First of all, review how <em>closed</em> type families are implemented in TypeScript:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">CloseTypeFamily</span><span class="o">&lt;</span><span class="nx">SomeType</span><span class="o">&gt;</span> <span class="o">=</span> <span class="nx">SomeType</span> <span class="kd">extends</span> <span class="kr">number</span> <span class="p">?</span> <span class="nx">boolean</span> <span class="p">:</span> <span class="kr">string</span><span class="p">;</span> </code></pre> </div> <p>This is one of the simplest type families in TypeScript. A type family is a function over a type, which receives types as arguments and returns another type. The example <code>CloseTypeFamily</code> receives a type as <code>SomeType</code>, and if <code>SomeType</code> is <code>number</code> (or a subtype of <code>number</code>), it returns the <code>boolean</code> type, then returns the <code>string</code> type otherwise.</p> <p>This is all about closed type families. They are just functions receiving and returning types. TypeScript doesn't have any special keyword for type families and is written so easily as above, therefore few people refer to the concept explicitly.</p> <p>Next, it's time for open type families. Let me show an open type family by writing in a pseudo-TypeScript supporting it:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">OpenTypeFamily</span><span class="o">&lt;</span><span class="nx">SomeType</span> <span class="kd">extends</span> <span class="kr">number</span><span class="o">&gt;</span> <span class="o">=</span> <span class="nx">boolean</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">OpenTypeFamily</span><span class="o">&lt;</span><span class="nx">SomeType</span> <span class="kd">extends</span> <span class="kr">string</span><span class="o">&gt;</span> <span class="o">=</span> <span class="nx">object</span><span class="p">[];</span> <span class="kd">type</span> <span class="nx">OpenTypeFamily</span><span class="o">&lt;</span><span class="nx">SomeType</span> <span class="kd">extends</span> <span class="kr">any</span><span class="p">[]</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">another</span><span class="p">:</span> <span class="dl">"</span><span class="s2">case</span><span class="dl">"</span><span class="p">,</span> <span class="na">youCan</span><span class="p">:</span> <span class="dl">"</span><span class="s2">add</span><span class="dl">"</span> <span class="p">};</span> </code></pre> </div> <p>Notice that there are multiple <code>type</code> declarations with the same name <code>OpenTypeFamily</code>, which TypeScript prohibits. This is open type families' indispensable feature that they are open to extending by adding other conditions of the arguments and the return type when the arguments satisfy them.</p> <p>How does <code>OpenTypeFamily</code> behave after being declared multiple times? If I were the designer of the open type families feature of TypeScript, I would make it do like below:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">OpenTypeFamily</span><span class="o">&lt;</span><span class="kr">number</span><span class="o">&gt;</span> <span class="c1">// Returns boolean.</span> <span class="kd">type</span> <span class="nx">OpenTypeFamily</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span> <span class="c1">// Returns object[].</span> <span class="kd">type</span> <span class="nx">OpenTypeFamily</span><span class="o">&lt;</span><span class="kr">number</span><span class="p">[]</span><span class="o">&gt;</span> <span class="c1">// Returns { another: "case", youCan: "add" }.</span> <span class="kd">type</span> <span class="nx">OpenTypeFamily</span><span class="o">&lt;</span><span class="nx">boolean</span><span class="o">&gt;</span> <span class="c1">// Type error because no matching argument is provided.</span> </code></pre> </div> <p>I omit the detailed rationale here because it's not important for this article. But speaking in short, an open type family searches all the declared conditions for one matching best with the given argument types to decide which type to return.</p> <h4> Simulate Open Type Families Using Global Interfaces </h4> <p>As described at the beginning of the last section, TypeScript doesn't provide open type families. But it doesn't actually need the complete functionality of the open type families just to reproduce "Trees that Grow". TypeScript already has a feature that resembles open type families as well as it can satisfy the requirement of "Trees that Grow".</p> <p>The answer is <code>interface</code>. We can use TypeScript's interface as a type family that receives a string literal type<sup id="fnref1">1</sup> to return a type:</p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kr">interface</span> <span class="nx">TypeFamily</span> <span class="p">{</span> <span class="nl">foo</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span> <span class="nl">bar</span><span class="p">:</span> <span class="nx">object</span><span class="p">[];</span> <span class="nl">baz</span><span class="p">:</span> <span class="p">{</span> <span class="na">another</span><span class="p">:</span> <span class="dl">"</span><span class="s2">type</span><span class="dl">"</span><span class="p">;</span> <span class="p">};</span> <span class="p">}</span> <span class="nx">TypeFamily</span><span class="p">[</span><span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">]</span> <span class="c1">// Returns boolean.</span> <span class="nx">TypeFamily</span><span class="p">[</span><span class="dl">"</span><span class="s2">bar</span><span class="dl">"</span><span class="p">]</span> <span class="c1">// Returns object[].</span> <span class="nx">TypeFamily</span><span class="p">[</span><span class="dl">"</span><span class="s2">baz</span><span class="dl">"</span><span class="p">]</span> <span class="c1">// Returns { another: "type"; }.</span> </code></pre> </div> <p><a href="proxy.php?url=https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgCoE8AOEBicC2wANusgN4BQy1yMA9nQFzIBGDREcIA3FTS3CjM6LAFYQEYANoBdXjVZwAXszLIudMAAtozAERgsEPd2QBfXmYoUOYdcwzY8hElL306euRQD0P6gB6APzWtqwORs7E6G4CUF68foEhNhB2CBFOBNGxygm+-sjBFEA">Try in the playground</a>.</p> <p>Thanks to <a href="proxy.php?url=https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html">Indexed Access Types</a>, we can see an interface as a type family that takes a key type via the indexing operator.</p> <p>We can pass even a parameterized key type:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kr">interface</span> <span class="nx">TypeFamily</span> <span class="p">{</span> <span class="nl">foo</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span> <span class="nl">bar</span><span class="p">:</span> <span class="nx">object</span><span class="p">[];</span> <span class="nl">baz</span><span class="p">:</span> <span class="p">{</span> <span class="na">another</span><span class="p">:</span> <span class="dl">"</span><span class="s2">type</span><span class="dl">"</span><span class="p">;</span> <span class="p">};</span> <span class="p">}</span> <span class="kd">type</span> <span class="nx">GetTheValueType</span><span class="o">&lt;</span><span class="nx">Key</span> <span class="kd">extends</span> <span class="kr">keyof</span> <span class="nx">TypeFamily</span><span class="o">&gt;</span> <span class="o">=</span> <span class="nx">TypeFamily</span><span class="p">[</span><span class="nx">Key</span><span class="p">];</span> <span class="nx">GetTheValueType</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="o">&gt;</span> <span class="c1">// Returns boolean.</span> <span class="nx">GetTheValueType</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">bar</span><span class="dl">"</span><span class="o">&gt;</span> <span class="c1">// Returns object[].</span> <span class="nx">GetTheValueType</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">baz</span><span class="dl">"</span><span class="o">&gt;</span> <span class="c1">// Returns { another: "type"; }.</span> </code></pre> </div> <p><a href="proxy.php?url=https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgCoE8AOEBicC2wANusgN4BQy1yMA9nQFzIBGDREcIA3FTS3CjM6LAFYQEYANoBdXjVZwAXszLIudMAAtozAERgsEPd2QBfXmYoVD2ZAHEIYVDoBqcIgFcIGbAB4AaQhSCAAPSBAAEwBnZABrYLoYNCM8QhIAPmQAXhTsNOJ0KSD0OWsOMHVmR2c3D29fCD89ejo9DN4Aek7qAD0AfnKnVmqnFwh3Lx8jZoEodq6e5AGhyoRR2on66f89ASUFim6+-qA">Try in the playground</a></p> <p>This is why an interface can be a type family. Then, how about its openness? To tell the truth, an interface is by design open to extending:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="c1">// Append to the last example</span> <span class="kr">interface</span> <span class="nx">TypeFamily</span> <span class="p">{</span> <span class="nl">anotherProperty</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">}</span> <span class="nx">GetTheValueType</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">anotherProperty</span><span class="dl">"</span><span class="o">&gt;</span> <span class="c1">// Returns string.</span> </code></pre> </div> <p><a href="proxy.php?url=https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgCoE8AOEBicC2wANusgN4BQy1yMA9nQFzIBGDREcIA3FTS3CjM6LAFYQEYANoBdXjVZwAXszLIudMAAtozAERgsEPd2QBfXmYoVD2ZAHEIYVDoBqcIgFcIGbAB4AaQhSCAAPSBAAEwBnZABrYLoYNCM8QhIAPmQAXhTsNOJ0KSD0OWsOMHVmR2c3D29fCD89ejo9DN4Aek7qAD0AfnKnVmqnFwh3Lx8jZoEodq6e5AGhyoRR2on66f89ASUFim6+wYpQSFhEFEaCknI+ag1taAAFKDpsKENmaLAoUAA5pZVshIhtxpMGjM9E8dFA3h9oIZDsdlv0gA">Try in the playground</a></p> <p>If a module contains several <code>interface</code> declarations with the same name, TypeScript interprets them as a unified single <code>interface</code> (See <a href="proxy.php?url=https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces">Merging Interfaces in the document</a> for details).</p> <p>But there is still an obstacle: Merging declarations of <code>interface</code> is limited within the module. This makes interface not suitable for "Trees that Grow" because it requires the type family extendable across modules (I'll show that with an example later).</p> <p>For a workaround for this issue, we have to declare the interface within a <code>declare global</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kr">declare</span> <span class="nb">global</span> <span class="p">{</span> <span class="kr">interface</span> <span class="nx">TypeFamily</span> <span class="p">{</span> <span class="nl">foo</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span> <span class="nl">bar</span><span class="p">:</span> <span class="nx">object</span><span class="p">[];</span> <span class="nl">baz</span><span class="p">:</span> <span class="p">{</span> <span class="na">another</span><span class="p">:</span> <span class="dl">"</span><span class="s2">type</span><span class="dl">"</span><span class="p">;</span> <span class="p">};</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">// ... In another file ...</span> <span class="kr">declare</span> <span class="nb">global</span> <span class="p">{</span> <span class="kr">interface</span> <span class="nx">TypeFamily</span> <span class="p">{</span> <span class="nl">anotherProperty</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p><code>declare global</code> makes <code>TypeFamily</code> literally "globally available"<sup id="fnref2">2</sup>. Now we can append new definitions to <code>TypeFamily</code> anywhere anytime.</p> <h4> Make <code>Expression</code> Extensible with a Global Interface </h4> <p>In the last section, we finally got all stuff necessary to implement "Trees that Grow". It's time to introduce the solution for the "Tree-Decoration Problem" in TypeScript using global interfaces to simulate open type families.</p> <p>To begin with, I'll show you the original, non-extensible <code>Expression</code> type again:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">Expression</span> <span class="o">=</span> <span class="nx">Literal</span> <span class="o">|</span> <span class="nx">Variable</span> <span class="o">|</span> <span class="nx">SetVariable</span> <span class="o">|</span> <span class="nx">Func</span> <span class="o">|</span> <span class="nx">CallFunc</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">Literal</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Literal</span><span class="dl">"</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">Variable</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Variable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">SetVariable</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">SetVariable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">Func</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Func</span><span class="dl">"</span><span class="p">;</span> <span class="nl">argumentName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">body</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">CallFunc</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">CallFunc</span><span class="dl">"</span><span class="p">;</span> <span class="nl">function</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">;</span> <span class="nl">argument</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">;</span> <span class="p">};</span> </code></pre> </div> <p>Let me update to make this type extensible step by step in the following paragraphs.</p> <p>First, add a new type parameter to every type of node of <code>Expression</code> and <code>Expression</code> itself:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span> <span class="o">=</span> <span class="o">|</span> <span class="nx">Literal</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">Variable</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">SetVariable</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">Func</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">CallFunc</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">Literal</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Literal</span><span class="dl">"</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">Variable</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Variable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">SetVariable</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">SetVariable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">Func</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Func</span><span class="dl">"</span><span class="p">;</span> <span class="nl">argumentName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">body</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">CallFunc</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">CallFunc</span><span class="dl">"</span><span class="p">;</span> <span class="nl">function</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span><span class="p">;</span> <span class="nl">argument</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">};</span> </code></pre> </div> <p>Second, introduce a global interface with just one property for each type of node:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kr">declare</span> <span class="nb">global</span> <span class="p">{</span> <span class="kr">interface</span> <span class="nx">ExtendExpression</span> <span class="p">{</span> <span class="nl">plain</span><span class="p">:</span> <span class="nx">object</span><span class="p">;</span> <span class="p">}</span> <span class="kr">interface</span> <span class="nx">ExtendLiteral</span> <span class="p">{</span> <span class="nl">plain</span><span class="p">:</span> <span class="nx">object</span><span class="p">;</span> <span class="p">}</span> <span class="kr">interface</span> <span class="nx">ExtendVariable</span> <span class="p">{</span> <span class="nl">plain</span><span class="p">:</span> <span class="nx">object</span><span class="p">;</span> <span class="p">}</span> <span class="kr">interface</span> <span class="nx">ExtendSetVariable</span> <span class="p">{</span> <span class="nl">plain</span><span class="p">:</span> <span class="nx">object</span><span class="p">;</span> <span class="p">}</span> <span class="kr">interface</span> <span class="nx">ExtendFunc</span> <span class="p">{</span> <span class="nl">plain</span><span class="p">:</span> <span class="nx">object</span><span class="p">;</span> <span class="p">}</span> <span class="kr">interface</span> <span class="nx">ExtendCallFunc</span> <span class="p">{</span> <span class="nl">plain</span><span class="p">:</span> <span class="nx">object</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>Those <code>Extend*</code> interfaces will serve as open type families. The only property <code>plain</code> represents "no extensions", used as the default extension to <code>Expression</code>.</p> <p>Next, restrict the type parameter to each node type of <code>Expression</code> to be one of the keys of the <code>Extend*</code> interfaces:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Descriptor</span> <span class="kd">extends</span> <span class="kr">keyof</span> <span class="nx">ExtendExpression</span><span class="o">&gt;</span> <span class="o">=</span> <span class="o">|</span> <span class="nx">Literal</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">Variable</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">SetVariable</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">Func</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">CallFunc</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">Literal</span><span class="o">&lt;</span><span class="nx">Descriptor</span> <span class="kd">extends</span> <span class="kr">keyof</span> <span class="nx">ExtendLiteral</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Literal</span><span class="dl">"</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">Variable</span><span class="o">&lt;</span><span class="nx">Descriptor</span> <span class="kd">extends</span> <span class="kr">keyof</span> <span class="nx">ExtendVariable</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Variable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">SetVariable</span><span class="o">&lt;</span><span class="nx">Descriptor</span> <span class="kd">extends</span> <span class="kr">keyof</span> <span class="nx">ExtendSetVariable</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">SetVariable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">Func</span><span class="o">&lt;</span><span class="nx">Descriptor</span> <span class="kd">extends</span> <span class="kr">keyof</span> <span class="nx">ExtendFunc</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Func</span><span class="dl">"</span><span class="p">;</span> <span class="nl">argumentName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">body</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">};</span> <span class="kd">type</span> <span class="nx">CallFunc</span><span class="o">&lt;</span><span class="nx">Descriptor</span> <span class="kd">extends</span> <span class="kr">keyof</span> <span class="nx">ExtendCallFunc</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">CallFunc</span><span class="dl">"</span><span class="p">;</span> <span class="nl">function</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span><span class="p">;</span> <span class="nl">argument</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">};</span> </code></pre> </div> <p>Note that each node type takes a differently restricted type parameter: <code>ExtendLiteral</code> for <code>Literal</code>, <code>ExtendVariable</code> for <code>Variable</code>, etc. This is the important point that enables us to append different properties to each node.</p> <p>And, as the final step, extend every type of node with the corresponding <code>Extend*</code> interface using <a href="proxy.php?url=https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types">the intersection operator <code>&amp;</code></a> and pass the type parameter <code>Descriptor</code> as an indexed access type of the <code>Extend*</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">Literal</span><span class="o">&lt;</span><span class="nx">Descriptor</span> <span class="kd">extends</span> <span class="kr">keyof</span> <span class="nx">ExtendLiteral</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Literal</span><span class="dl">"</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">ExtendLiteral</span><span class="p">[</span><span class="nx">Descriptor</span><span class="p">];</span> <span class="c1">// &lt;- Append this!</span> <span class="kd">type</span> <span class="nx">Variable</span><span class="o">&lt;</span><span class="nx">Descriptor</span> <span class="kd">extends</span> <span class="kr">keyof</span> <span class="nx">ExtendVariable</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Variable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">ExtendVariable</span><span class="p">[</span><span class="nx">Descriptor</span><span class="p">];</span> <span class="c1">// &lt;- Append this!</span> <span class="kd">type</span> <span class="nx">SetVariable</span><span class="o">&lt;</span><span class="nx">Descriptor</span> <span class="kd">extends</span> <span class="kr">keyof</span> <span class="nx">ExtendSetVariable</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">SetVariable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">ExtendSetVariable</span><span class="p">[</span><span class="nx">Descriptor</span><span class="p">];</span> <span class="c1">// &lt;- Append this!</span> <span class="kd">type</span> <span class="nx">Func</span><span class="o">&lt;</span><span class="nx">Descriptor</span> <span class="kd">extends</span> <span class="kr">keyof</span> <span class="nx">ExtendFunc</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Func</span><span class="dl">"</span><span class="p">;</span> <span class="nl">argumentName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">body</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">ExtendFunc</span><span class="p">[</span><span class="nx">Descriptor</span><span class="p">];</span> <span class="c1">// &lt;- Append this!</span> <span class="kd">type</span> <span class="nx">CallFunc</span><span class="o">&lt;</span><span class="nx">Descriptor</span> <span class="kd">extends</span> <span class="kr">keyof</span> <span class="nx">ExtendCallFunc</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">CallFunc</span><span class="dl">"</span><span class="p">;</span> <span class="nl">function</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span><span class="p">;</span> <span class="nl">argument</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Descriptor</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">ExtendCallFunc</span><span class="p">[</span><span class="nx">Descriptor</span><span class="p">];</span> <span class="c1">// &lt;- Append this!</span> </code></pre> </div> <p>Then, how can we add properties as we like? Extend all global <code>Extend*</code> interfaces. For example, to add the <code>namespace</code> property only to <code>Variable</code> and <code>SetVariable</code>, add it to <code>Extend*</code> interfaces:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kr">declare</span> <span class="nb">global</span> <span class="p">{</span> <span class="kr">interface</span> <span class="nx">ExtendExpression</span> <span class="p">{</span> <span class="nl">namespace</span><span class="p">:</span> <span class="nx">object</span><span class="p">;</span> <span class="p">}</span> <span class="kr">interface</span> <span class="nx">ExtendLiteral</span> <span class="p">{</span> <span class="nl">namespace</span><span class="p">:</span> <span class="nx">object</span><span class="p">;</span> <span class="p">}</span> <span class="kr">interface</span> <span class="nx">ExtendVariable</span> <span class="p">{</span> <span class="nl">namespace</span><span class="p">:</span> <span class="p">{</span> <span class="na">qualifier</span><span class="p">:</span> <span class="kr">string</span> <span class="p">};</span> <span class="p">}</span> <span class="kr">interface</span> <span class="nx">ExtendSetVariable</span> <span class="p">{</span> <span class="nl">namespace</span><span class="p">:</span> <span class="p">{</span> <span class="na">qualifier</span><span class="p">:</span> <span class="kr">string</span> <span class="p">};</span> <span class="p">}</span> <span class="kr">interface</span> <span class="nx">ExtendFunc</span> <span class="p">{</span> <span class="nl">namespace</span><span class="p">:</span> <span class="nx">object</span><span class="p">;</span> <span class="p">}</span> <span class="kr">interface</span> <span class="nx">ExtendCallFunc</span> <span class="p">{</span> <span class="nl">namespace</span><span class="p">:</span> <span class="nx">object</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>To leave the other types of nodes unextended, their <code>namespace</code> is <code>object</code>, which stands for "any object". <a href="proxy.php?url=https://typescript-eslint.io/rules/ban-types">As typescript-eslint recommends</a>, I use <code>object</code> instead of <code>{}</code> for "any object type", which is the identity element (which doesn't change anything that is applied with) of the intersection operator <code>&amp;</code>.</p> <p>Now, see the result by actually using the <code>Expression</code> type by passing the name of the new property:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">MyExpression</span> <span class="o">=</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">namespace</span><span class="dl">"</span><span class="o">&gt;</span><span class="p">;</span> </code></pre> </div> <p>Let's confirm how <code>Expression</code> is extended by expanding the <code>type</code> aliases step by step:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="c1">// (1) By definition of `Expression`,</span> <span class="c1">// replace the `Descriptor` type argument to `"namespace"`:</span> <span class="kd">type</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">namespace</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">=</span> <span class="o">|</span> <span class="nx">Literal</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">namespace</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">Variable</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">namespace</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">SetVariable</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">namespace</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">Func</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">namespace</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">CallFunc</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">namespace</span><span class="dl">"</span><span class="o">&gt;</span><span class="p">;</span> <span class="c1">// (2A) By definition of `Literal`:</span> <span class="kd">type</span> <span class="nx">Literal</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">namespace</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Literal</span><span class="dl">"</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">ExtendLiteral</span><span class="p">[</span><span class="dl">"</span><span class="s2">namespace</span><span class="dl">"</span><span class="p">];</span> <span class="c1">// (3A) By definition of `ExtendLiteral`,</span> <span class="c1">// `ExtendLiteral["namespace"]` is `object`:</span> <span class="kd">type</span> <span class="nx">Literal</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">namespace</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Literal</span><span class="dl">"</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">object</span><span class="p">;</span> <span class="c1">// (4A) Appending `object` with `&amp;` makes no change:</span> <span class="kd">type</span> <span class="nx">Literal</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">namespace</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Literal</span><span class="dl">"</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="p">};</span> </code></pre> </div> <p>(2A)-(4A) applies to the <code>Func</code> and <code>CallFunc</code> types. Go on to expand the <code>Variable</code> type:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="cm">/* (Continued from (1) above) */</span> <span class="c1">// (2B) By definition of `Variable`:</span> <span class="kd">type</span> <span class="nx">Variable</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">namespace</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Variable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">ExtendVariable</span><span class="p">[</span><span class="dl">"</span><span class="s2">namespace</span><span class="dl">"</span><span class="p">];</span> <span class="c1">// (3B) By definition of `ExtendVariable`,</span> <span class="c1">// `ExtendVariable["namespace"]` is `{ qualifier: string }`.</span> <span class="kd">type</span> <span class="nx">Variable</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">namespace</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Variable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="p">{</span> <span class="na">qualifier</span><span class="p">:</span> <span class="kr">string</span> <span class="p">};</span> <span class="c1">// (4B) By definition of the `&amp;` operator:</span> <span class="kd">type</span> <span class="nx">Variable</span><span class="o">&lt;</span><span class="dl">"</span><span class="s2">namespace</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Variable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">qualifier</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">}</span> </code></pre> </div> <p>(2B)-(3B) applies to the <code>SetVariable</code> type.</p> <p>As the expansion before shows, only the <code>Variable</code> and <code>SetVariable</code> type is extended by the <code>Expand*</code> interfaces. Consequently, we found a method to solve the "Tree-Decoration Problem" by extending each globally declared interface.</p> <h4> New Problem: Can We Make it Simpler? </h4> <p>The workaround I explained in the previous sections is a port of the "Trees that Grow" technique in Haskell to TypeScript. It allows us to freely extend nested recursive types with additional properties just as necessary for applications using the types, via open type families (globally appendable interfaces in TypeScript).</p> <p>But it involves a new problem: using <code>declare global</code>-ed interfaces is a very indirect way. Code readers that see the global interfaces for the first type would have difficulties in finding the relationship between the type to extend and them, similar to traditional global variables.</p> <p>You might get confused by my explanation, to wish for an easier-to-understand solution. I'll invent and develop a much more succinct solution for TypeScript in the following sections.</p> <h3> Incomplete Solution 2: Parameterize Every Extension to Pass Around </h3> <p>As a first step, review again the "Incomplete Solution 1: Parameterize an Extension to Pass Around":<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">=</span> <span class="nx">Literal</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">Variable</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">SetVariable</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">Func</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">CallFunc</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">Literal</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Literal</span><span class="dl">"</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">Extension</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">Variable</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Variable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">Extension</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">SetVariable</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">SetVariable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">Extension</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">Func</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Func</span><span class="dl">"</span><span class="p">;</span> <span class="nl">argumentName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">body</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">Extension</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">CallFunc</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">CallFunc</span><span class="dl">"</span><span class="p">;</span> <span class="nl">function</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span><span class="p">;</span> <span class="nl">argument</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Extension</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">Extension</span><span class="p">;</span> </code></pre> </div> <p>The problem with this solution is that it cannot switch the extension type by a different type of node because <code>Expression</code> has only one type parameter.</p> <p>In other words, that means it can fix the problem by passing type parameters <strong>as many as the node types</strong>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">Expression</span><span class="o">&lt;</span> <span class="nx">ExtendLiteral</span><span class="p">,</span> <span class="nx">ExtendVariable</span><span class="p">,</span> <span class="nx">ExtendSetVariable</span><span class="p">,</span> <span class="nx">ExtendFunc</span><span class="p">,</span> <span class="nx">ExtendCallFunc</span><span class="p">,</span> <span class="o">&gt;</span> <span class="o">=</span> <span class="o">|</span> <span class="nx">Literal</span><span class="o">&lt;</span><span class="nx">ExtendLiteral</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">Variable</span><span class="o">&lt;</span><span class="nx">ExtendVariable</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">SetVariable</span><span class="o">&lt;</span> <span class="nx">ExtendLiteral</span><span class="p">,</span> <span class="nx">ExtendVariable</span><span class="p">,</span> <span class="nx">ExtendSetVariable</span><span class="p">,</span> <span class="nx">ExtendFunc</span><span class="p">,</span> <span class="nx">ExtendCallFunc</span> <span class="o">&gt;</span> <span class="o">|</span> <span class="nx">Func</span><span class="o">&lt;</span> <span class="nx">ExtendLiteral</span><span class="p">,</span> <span class="nx">ExtendVariable</span><span class="p">,</span> <span class="nx">ExtendSetVariable</span><span class="p">,</span> <span class="nx">ExtendFunc</span><span class="p">,</span> <span class="nx">ExtendCallFunc</span> <span class="o">&gt;</span> <span class="o">|</span> <span class="nx">CallFunc</span><span class="o">&lt;</span> <span class="nx">ExtendLiteral</span><span class="p">,</span> <span class="nx">ExtendVariable</span><span class="p">,</span> <span class="nx">ExtendSetVariable</span><span class="p">,</span> <span class="nx">ExtendFunc</span><span class="p">,</span> <span class="nx">ExtendCallFunc</span> <span class="o">&gt;</span><span class="p">;</span> <span class="c1">// ... Types of nodes go on ...</span> </code></pre> </div> <p>You'll find the type parameters (<code>ExtendLieteral, ExtendVariable, ...</code>) are so repetitiously referenced at the first glance. This is because <code>SetVariable</code>, <code>Func</code>, and <code>CallFunc</code> contain <code>Expression</code> as their child nodes.</p> <p>And as a matter of course, the type parameters have to be passed down again in their definition. For example in <code>CallFunc</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">CallFunc</span><span class="o">&lt;</span> <span class="nx">ExtendLiteral</span><span class="p">,</span> <span class="nx">ExtendVariable</span><span class="p">,</span> <span class="nx">ExtendSetVariable</span><span class="p">,</span> <span class="nx">ExtendFunc</span><span class="p">,</span> <span class="nx">ExtendCallFunc</span><span class="p">,</span> <span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">CallFunc</span><span class="dl">"</span><span class="p">;</span> <span class="nl">function</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span> <span class="nx">ExtendLiteral</span><span class="p">,</span> <span class="nx">ExtendVariable</span><span class="p">,</span> <span class="nx">ExtendSetVariable</span><span class="p">,</span> <span class="nx">ExtendFunc</span><span class="p">,</span> <span class="nx">ExtendCallFunc</span> <span class="o">&gt;</span><span class="p">;</span> <span class="nl">argument</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span> <span class="nx">ExtendLiteral</span><span class="p">,</span> <span class="nx">ExtendVariable</span><span class="p">,</span> <span class="nx">ExtendSetVariable</span><span class="p">,</span> <span class="nx">ExtendFunc</span><span class="p">,</span> <span class="nx">ExtendCallFunc</span> <span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">ExtendCallFunc</span><span class="p">;</span> </code></pre> </div> <p>The repetition of the type arguments is not only noisy but also hard to maintain. We have to keep the order of the parameters consistent across the node types anytime when adding/removing a node type. This is unsatisfactory.</p> <h3> Final Solution: Packing up Type Parameters Into One Structure </h3> <p>From our programming experience with an ordinary function, we usually fix such a problem by making all the arguments available as a single structure to pass around to all the callee functions:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">Arguments</span> <span class="o">=</span> <span class="p">{</span> <span class="na">foo</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">bar</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">baz</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span> <span class="nl">qux</span><span class="p">:</span> <span class="kr">number</span> <span class="o">|</span> <span class="kc">undefined</span><span class="p">;</span> <span class="nl">quux</span><span class="p">:</span> <span class="kr">string</span><span class="p">[];</span> <span class="p">};</span> <span class="kd">function</span> <span class="nx">callee1</span><span class="p">(</span><span class="nx">arguments</span><span class="p">:</span> <span class="nx">Arguments</span><span class="p">);</span> <span class="kd">function</span> <span class="nx">callee2</span><span class="p">(</span><span class="nx">arguments</span><span class="p">:</span> <span class="nx">Arguments</span><span class="p">);</span> <span class="kd">function</span> <span class="nx">rootCaller</span><span class="p">(</span><span class="nx">arguments</span><span class="p">:</span> <span class="nx">Arguments</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="nx">callee1</span><span class="p">(</span><span class="nx">arguments</span><span class="p">);</span> <span class="c1">// ...</span> <span class="nx">callee2</span><span class="p">(</span><span class="nx">arguments</span><span class="p">);</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="c1">// v.s. Unpacking all the arguments like below:</span> <span class="kd">function</span> <span class="nx">callee1</span><span class="p">(</span><span class="nx">foo</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">bar</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">baz</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">,</span> <span class="nx">qux</span><span class="p">:</span> <span class="kr">number</span> <span class="o">|</span> <span class="kc">undefined</span><span class="p">,</span> <span class="nx">quux</span><span class="p">:</span> <span class="kr">string</span><span class="p">[]);</span> <span class="kd">function</span> <span class="nx">callee2</span><span class="p">(</span><span class="nx">foo</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">bar</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">baz</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">,</span> <span class="nx">qux</span><span class="p">:</span> <span class="kr">number</span> <span class="o">|</span> <span class="kc">undefined</span><span class="p">,</span> <span class="nx">quux</span><span class="p">:</span> <span class="kr">string</span><span class="p">[]);</span> <span class="kd">function</span> <span class="nx">rootCaller</span><span class="p">(</span><span class="nx">foo</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">bar</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">baz</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">,</span> <span class="nx">qux</span><span class="p">:</span> <span class="kr">number</span> <span class="o">|</span> <span class="kc">undefined</span><span class="p">,</span> <span class="nx">quux</span><span class="p">:</span> <span class="kr">string</span><span class="p">[])</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="nx">callee1</span><span class="p">(</span><span class="nx">foo</span><span class="p">,</span> <span class="nx">bar</span><span class="p">,</span> <span class="nx">baz</span><span class="p">,</span> <span class="nx">qux</span><span class="p">,</span> <span class="nx">quux</span><span class="p">);</span> <span class="c1">// ...</span> <span class="nx">callee2</span><span class="p">(</span><span class="nx">foo</span><span class="p">,</span> <span class="nx">bar</span><span class="p">,</span> <span class="nx">baz</span><span class="p">,</span> <span class="nx">qux</span><span class="p">,</span> <span class="nx">quux</span><span class="p">);</span> <span class="c1">// ...</span> <span class="p">}</span> </code></pre> </div> <p>In Haskell, we have to reproduce that indirectly via open type families. Meanwhile, in TypeScript, we can easily achieve the goal by combining an interface with Indexed Access Types in fact:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="c1">// Interface with one property per node type.</span> <span class="kr">interface</span> <span class="nx">ExtendExpression</span> <span class="p">{</span> <span class="nl">Literal</span><span class="p">:</span> <span class="nx">object</span><span class="p">;</span> <span class="nl">Variable</span><span class="p">:</span> <span class="nx">object</span><span class="p">;</span> <span class="nl">SetVariable</span><span class="p">:</span> <span class="nx">object</span><span class="p">;</span> <span class="nl">Func</span><span class="p">:</span> <span class="nx">object</span><span class="p">;</span> <span class="nl">CallFunc</span><span class="p">:</span> <span class="nx">object</span><span class="p">;</span> <span class="p">}</span> <span class="c1">// Specify the default type for `Extend` to make it easier to use.</span> <span class="kd">type</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Extend</span> <span class="kd">extends</span> <span class="nx">ExtendExpression</span> <span class="o">=</span> <span class="nx">ExtendExpression</span><span class="o">&gt;</span> <span class="o">=</span> <span class="o">|</span> <span class="nx">Literal</span><span class="o">&lt;</span><span class="nx">Extend</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">Variable</span><span class="o">&lt;</span><span class="nx">Extend</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">SetVariable</span><span class="o">&lt;</span><span class="nx">Extend</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">Func</span><span class="o">&lt;</span><span class="nx">Extend</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">CallFunc</span><span class="o">&lt;</span><span class="nx">Extend</span><span class="o">&gt;</span><span class="p">;</span> <span class="kd">type</span> <span class="nx">Literal</span><span class="o">&lt;</span><span class="nx">Extend</span> <span class="kd">extends</span> <span class="nx">ExtendExpression</span> <span class="o">=</span> <span class="nx">ExtendExpression</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Literal</span><span class="dl">"</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">Extend</span><span class="p">[</span><span class="dl">"</span><span class="s2">Literal</span><span class="dl">"</span><span class="p">];</span> <span class="kd">type</span> <span class="nx">Variable</span><span class="o">&lt;</span><span class="nx">Extend</span> <span class="kd">extends</span> <span class="nx">ExtendExpression</span> <span class="o">=</span> <span class="nx">ExtendExpression</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Variable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">Extend</span><span class="p">[</span><span class="dl">"</span><span class="s2">Variable</span><span class="dl">"</span><span class="p">];</span> <span class="kd">type</span> <span class="nx">SetVariable</span><span class="o">&lt;</span><span class="nx">Extend</span> <span class="kd">extends</span> <span class="nx">ExtendExpression</span> <span class="o">=</span> <span class="nx">ExtendExpression</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">SetVariable</span><span class="dl">"</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Extend</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">Extend</span><span class="p">[</span><span class="dl">"</span><span class="s2">SetVariable</span><span class="dl">"</span><span class="p">];</span> <span class="kd">type</span> <span class="nx">Func</span><span class="o">&lt;</span><span class="nx">Extend</span> <span class="kd">extends</span> <span class="nx">ExtendExpression</span> <span class="o">=</span> <span class="nx">ExtendExpression</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Func</span><span class="dl">"</span><span class="p">;</span> <span class="nl">argumentName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">body</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Extend</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">Extend</span><span class="p">[</span><span class="dl">"</span><span class="s2">Func</span><span class="dl">"</span><span class="p">];</span> <span class="kd">type</span> <span class="nx">CallFunc</span><span class="o">&lt;</span><span class="nx">Extend</span> <span class="kd">extends</span> <span class="nx">ExtendExpression</span> <span class="o">=</span> <span class="nx">ExtendExpression</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">CallFunc</span><span class="dl">"</span><span class="p">;</span> <span class="nl">function</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Extend</span><span class="o">&gt;</span><span class="p">;</span> <span class="nl">argument</span><span class="p">:</span> <span class="nx">Expression</span><span class="o">&lt;</span><span class="nx">Extend</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="nx">Extend</span><span class="p">[</span><span class="dl">"</span><span class="s2">CallFunc</span><span class="dl">"</span><span class="p">];</span> </code></pre> </div> <p>Each property of the <code>ExtendExpression</code> interface is for the corresponding type of node. Hence, we can extract only part of the <code>Extend</code> type variable for each type of node, as well as we can handily pass around the <code>Extend</code> to the child node types.</p> <h4> Example </h4> <p>I created a trivial example with a function to convert a raw <code>Expression</code> into one whose variables are qualified with the namespace:</p> <p><a href="proxy.php?url=https://github.com/igrep/ts-that-grow/blob/main/src/app.ts">https://github.com/igrep/ts-that-grow/blob/main/src/app.ts</a></p> <p>Try and see how it works!</p> <h2> Final Thoughts: Really Impossible in Haskell? </h2> <p>You might be surprised at the simplicity of the final solution in comparison with the other ideas shown before. But according to the original "Trees that Grow" paper, Haskell requires a relatively complex method using open type families. It seems that the biggest reason is that Haskell doesn't provide functionality similar to TypeScript's packing type arguments into a single structure.</p> <p>From another perspective, the "packing" feature of TypeScript is a restricted version of higher-order type families (type families that takes type families, like higher-order functions) in a way: the <code>Extend</code> type variable of <code>Expression</code> can be seen as a type function that takes the name of its property and returns a type via Indexed Access Type.</p> <p>By contrast, Haskell doesn't support higher-order type families at least in a straightforward way, due to the limitation of its type families (Ref. <a href="proxy.php?url=https://stackoverflow.com/questions/71275294/higher-order-type-families-in-haskell">Higher order type families in Haskell - Stack Overflow</a>). But I might miss something: I'll write up a separate post if I find a new technique in Haskell.</p> <ol> <li id="fn1"> <p>Strictly speaking, the argument type can be any that can be the key of an object, such as <code>number</code> and <code>Symbol</code>. ↩</p> </li> <li id="fn2"> <p>I learned <code>declare global</code> in depth with <a href="proxy.php?url=https://zenn.dev/qnighy/articles/9c4ce0f1b68350">this article (in Japanese)</a>. ↩</p> </li> </ol> typescript types Why I failed to create the "Solid.js's store" for Svelte, and announcing svelte-store-tree v0.3.1 YAMAMOTO Yuji Tue, 08 Nov 2022 04:54:22 +0000 https://dev.to/igrep/why-i-failed-to-create-the-solidjss-store-for-svelte-and-announcing-svelte-store-tree-v031-1am2 https://dev.to/igrep/why-i-failed-to-create-the-solidjss-store-for-svelte-and-announcing-svelte-store-tree-v031-1am2 <p>Recently I released a new version of <a href="proxy.php?url=https://www.npmjs.com/package/svelte-store-tree" rel="noopener noreferrer">svelte-store-tree</a>. It's a state management library for Svelte that I started to develop two months ago, and redesigned its API in the latest version. Today, let me introduce the detailed usage of svelte-store-tree, and compare with a similar library: Solid.js's store feature. Then, I'll share you with a problem of Svelte's store. I'm glad if this article helps you review the design of Svelte in the future.</p> <h2> Summary </h2> <ul> <li>svelte-store-tree is a state management library for managing tree-like nested structures in Svelte.</li> <li>I referred a similar library, Solid.js's store, before creating it. But by contrast with Solid.js's store, svelte-store-tree requires selecting the value inside the store with its <code>choose</code> function to handle union type values. It's due to the design of Svelte's store that confines "the API to read the current value of the store" and "the API to update the value of the store" into a single store object.</li> </ul> <h2> Introduction to svelte-store-tree </h2> <p>As the name shows, svelte-store-tree adds<sup id="fnref1">1</sup> methods to <a href="proxy.php?url=https://svelte.dev/tutorial/writable-stores" rel="noopener noreferrer">the store objects of Svelte</a>, to easily work with trees (nested structures). I developed it for building a complex tree structure intuitively in my current job with Svelte.</p> <h3> Example Usage </h3> <p>From this section, I'll use the type below in <a href="proxy.php?url=https://codesandbox.io/p/github/igrep/svelte-store-tree/draft/floral-sound" rel="noopener noreferrer">the example app you can run here</a>.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">export</span> <span class="kd">type</span> <span class="nx">Tree</span> <span class="o">=</span> <span class="kr">string</span> <span class="o">|</span> <span class="kc">undefined</span> <span class="o">|</span> <span class="nx">KeyValue</span> <span class="o">|</span> <span class="nx">Tree</span><span class="p">[];</span> <span class="k">export</span> <span class="kd">type</span> <span class="nx">KeyValue</span> <span class="o">=</span> <span class="p">{</span> <span class="na">key</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">value</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">};</span> </code></pre> </div> <p>It's really a recursive structure with various possible values.</p> <p>NOTE: <a href="proxy.php?url=https://github.com/igrep/svelte-store-tree/tree/main/example" rel="noopener noreferrer">I uploaded the code of the example app onto GitHub</a>, so I'll put the link to the corresponding lines in GitHub after quoting the part of the code.</p> <h4> Creating a <code>WritableTree</code> Object </h4> <p>To create a <code>WritableTree</code> object provided by svelte-store-tree, use the <code>writableTree</code> function literally:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">tree</span> <span class="o">=</span> <span class="nx">writableTree</span><span class="o">&lt;</span><span class="nx">Tree</span><span class="o">&gt;</span><span class="p">([</span> <span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">bar</span><span class="dl">"</span><span class="p">,</span> <span class="p">[</span><span class="dl">"</span><span class="s2">baz1</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">baz2</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">baz3</span><span class="dl">"</span><span class="p">],</span> <span class="kc">undefined</span><span class="p">,</span> <span class="p">{</span> <span class="na">key</span><span class="p">:</span> <span class="dl">"</span><span class="s2">some key</span><span class="dl">"</span><span class="p">,</span> <span class="na">value</span><span class="p">:</span> <span class="dl">"</span><span class="s2">some value</span><span class="dl">"</span> <span class="p">},</span> <span class="p">]);</span> </code></pre> </div> <p>In the same way with the original <code>writable</code> function, <code>writableTree</code> receives the initial value of the store as its argument. Because <code>Tree</code> is a union type, we should specify the type parameter to help type inference in the example above.</p> <h4> <code>subscribe</code> and <code>set</code> like the original store of Svelte </h4> <p>svelte-store-tree complies with <a href="proxy.php?url=https://svelte.dev/docs#component-format-script-4-prefix-stores-with-$-to-access-their-values-store-contract" rel="noopener noreferrer">the store contract of Svelte</a>. Accordingly prefixing the variable name with a dollar sign <code>$</code>, we can <code>subscribe</code> <code>WritableTree</code>/<code>ReadableTree</code>s and <code>set</code> <code>WritableTree</code>s. Of course two-way binding to the part of the store tree is also available!<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight html"><code><span class="nt">&lt;script </span><span class="na">lang=</span><span class="s">"ts"</span><span class="nt">&gt;</span> <span class="k">export</span> <span class="kd">let</span> <span class="nx">tree</span><span class="p">:</span> <span class="nx">WritableTree</span><span class="o">&lt;</span><span class="nx">Tree</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">...</span> <span class="nt">&lt;/script&gt;</span> ... {#if typeof $tree === "string"} <span class="nt">&lt;li&gt;</span> <span class="nt">&lt;NodeTypeSelector</span> <span class="na">label=</span><span class="s">"Switch"</span> <span class="na">bind:selected</span> <span class="err">{</span><span class="na">onSelected</span><span class="err">}</span> <span class="nt">/&gt;&lt;input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">bind:value=</span><span class="s">{$tree}</span> <span class="nt">/&gt;</span> <span class="nt">&lt;/li&gt;</span> ... {/if} </code></pre> </div> <p><a href="proxy.php?url=https://github.com/igrep/svelte-store-tree/blob/30740be58afe4413f0d4e2d454bfe2b340a929e4/example/Tree.svelte#L36-L42" rel="noopener noreferrer">See in GitHub</a></p> <h4> Create a <code>WritableTree</code> for a part of the tree by <code>zoom</code> </h4> <p>svelte-store-tree provides a way to make a store for some part of the tree as these methods whose name begins with <code>zoom</code>:</p> <ul> <li> <code>zoom&lt;C&gt;(accessor: Accessor&lt;P, C&gt;): WritableTree&lt;C&gt;</code> <ul> <li>Returns a new <code>WritableTree</code> object with an <code>Accessor</code> object.</li> <li>An <code>Accessor</code> object implements a method to get the child <code>C</code> from the parent <code>P</code>, and a method to replace the child <code>C</code> of the parent <code>P</code>.</li> </ul> </li> <li> <code>zoomNoSet&lt;C&gt;(readChild: (parent: P) =&gt; C | Refuse): ReadableTree&lt;C&gt;</code> <ul> <li>Takes a function to get the child <code>C</code> from the parent <code>P</code> and then returns a <code>ReadableTree</code> object.</li> <li> <code>ReadableTree</code> is a store tree that can't <code>set</code>. But child store trees <code>zoom</code>ed from a <code>ReadableTree</code> can <code>set</code>. So it is NOT completely read-only.</li> </ul> </li> </ul> <p>In the example app, I created <code>WritableTree</code>s for writing the <code>key</code> and <code>value</code> field of the <code>KeyValue</code> object, using the <code>into</code> function for building <code>Accessor</code>s:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">tree</span><span class="p">.</span><span class="nf">zoom</span><span class="p">(</span><span class="nf">into</span><span class="p">(</span><span class="dl">"</span><span class="s2">key</span><span class="dl">"</span><span class="p">));</span> <span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="nx">tree</span><span class="p">.</span><span class="nf">zoom</span><span class="p">(</span><span class="nf">into</span><span class="p">(</span><span class="dl">"</span><span class="s2">value</span><span class="dl">"</span><span class="p">));</span> </code></pre> </div> <p>However, this isn't correct. svelte-check warns me of type errors detected by TypeScript:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="o">/</span><span class="nx">svelte</span><span class="o">-</span><span class="nx">store</span><span class="o">-</span><span class="nx">tree</span><span class="o">/</span><span class="nx">example</span><span class="o">/</span><span class="nx">Tree</span><span class="p">.</span><span class="nx">svelte</span><span class="p">:</span><span class="mi">14</span><span class="p">:</span><span class="mi">35</span> <span class="nb">Error</span><span class="p">:</span> <span class="nx">Argument</span> <span class="k">of</span> <span class="kd">type</span> <span class="dl">'</span><span class="s1">string</span><span class="dl">'</span> <span class="k">is</span> <span class="nx">not</span> <span class="nx">assignable</span> <span class="nx">to</span> <span class="nx">parameter</span> <span class="k">of</span> <span class="kd">type</span> <span class="dl">'</span><span class="s1">never</span><span class="dl">'</span><span class="p">.</span> <span class="p">(</span><span class="nx">ts</span><span class="p">)</span> <span class="p">...</span> <span class="o">/</span><span class="nx">svelte</span><span class="o">-</span><span class="nx">store</span><span class="o">-</span><span class="nx">tree</span><span class="o">/</span><span class="nx">example</span><span class="o">/</span><span class="nx">Tree</span><span class="p">.</span><span class="nx">svelte</span><span class="p">:</span><span class="mi">15</span><span class="p">:</span><span class="mi">37</span> <span class="nb">Error</span><span class="p">:</span> <span class="nx">Argument</span> <span class="k">of</span> <span class="kd">type</span> <span class="dl">'</span><span class="s1">string</span><span class="dl">'</span> <span class="k">is</span> <span class="nx">not</span> <span class="nx">assignable</span> <span class="nx">to</span> <span class="nx">parameter</span> <span class="k">of</span> <span class="kd">type</span> <span class="dl">'</span><span class="s1">never</span><span class="dl">'</span><span class="p">.</span> <span class="p">(</span><span class="nx">ts</span><span class="p">)</span> <span class="p">...</span> </code></pre> </div> <p>The messages are somewhat confusing as it says <code>parameter of type 'never'</code>. The type checker means the result of <code>keyof Tree</code> is <code>never</code> because the type of <code>tree</code> is <code>WritableTree&lt;Tree&gt;</code>, whose content <code>Tree</code> is by definition a union type that can be <code>undefined</code>.</p> <p>To fix the problem, we have to convert the <code>WritableTree&lt;Tree&gt;</code> into <code>WritableTree&lt;KeyValue&gt;</code> by some means. The <code>choose</code> function helps us in such a case.</p> <h4> Subscribe the value only fulfilling the condition using <code>choose</code> </h4> <p>We can create a store tree that calls the <code>subscribe</code>r functions only if the store value matches with the specific condition, by calling <code>zoom</code> with an <code>Accessor</code> from the <code>choose</code> function.</p> <p>Here I used the <code>choose</code> function in the example app:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="p">...</span> <span class="kd">const</span> <span class="nx">keyValue</span> <span class="o">=</span> <span class="nx">tree</span><span class="p">.</span><span class="nf">zoom</span><span class="p">(</span><span class="nf">choose</span><span class="p">(</span><span class="nx">chooseKeyValue</span><span class="p">));</span> <span class="p">...</span> </code></pre> </div> <p><code>choose</code> takes a function receiving a store value to return some other value, or <code>Refuse</code>. The <code>chooseKeyValue</code> function is the argument of <code>choose</code> in the case above. It's defined as following:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">export</span> <span class="kd">function</span> <span class="nf">chooseKeyValue</span><span class="p">(</span><span class="nx">tree</span><span class="p">:</span> <span class="nx">Tree</span><span class="p">):</span> <span class="nx">KeyValue</span> <span class="o">|</span> <span class="nx">Refuse</span> <span class="p">{</span> <span class="k">if </span><span class="p">(</span><span class="nx">tree</span> <span class="o">===</span> <span class="kc">undefined</span> <span class="o">||</span> <span class="k">typeof</span> <span class="nx">tree</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="o">||</span> <span class="nx">tree</span> <span class="k">instanceof</span> <span class="nb">Array</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">Refuse</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="nx">tree</span><span class="p">;</span> <span class="p">}</span> </code></pre> </div> <p><a href="proxy.php?url=https://github.com/igrep/svelte-store-tree/blob/30740be58afe4413f0d4e2d454bfe2b340a929e4/example/tree.ts#L62-L67" rel="noopener noreferrer">See in GitHub</a></p> <p>A <code>WritableTree</code> object returned by <code>choose</code> calls the <code>subscribe</code>r functions when the result of the function (passed to <code>choose</code>) is not <code>Refuse</code>, a unique symbol dedicated for this purpose<sup id="fnref2">2</sup>.</p> <p>You might wonder why <code>choose</code> doesn't judge by a boolean value or refuse <code>undefined</code> instead of the unique symbol. First, the function given to <code>choose</code> must specify the return type to make use of <a href="proxy.php?url=https://www.typescriptlang.org/docs/handbook/2/narrowing.html" rel="noopener noreferrer">the narrowing feature of TypeScript</a>. The <a href="proxy.php?url=https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards" rel="noopener noreferrer">user-defined type guard</a> doesn't help us with a higher-order function like <code>choose</code>. Second, I made the unique symbol <code>Refuse</code> to handle objects with nullable properties such as <code>{ property: T | undefined }</code> properly.</p> <p>Let's get back to the problem of converting the <code>WritableTree&lt;Tree&gt;</code> into <code>WritableTree&lt;KeyValue&gt;</code>. The error by <code>tree.zoom(into("key"))</code> is corrected using <code>choose</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">keyValue</span> <span class="o">=</span> <span class="nx">tree</span><span class="p">.</span><span class="nf">zoom</span><span class="p">(</span><span class="nf">choose</span><span class="p">(</span><span class="nx">chooseKeyValue</span><span class="p">));</span> <span class="kd">const</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">keyValue</span><span class="p">.</span><span class="nf">zoom</span><span class="p">(</span><span class="nf">into</span><span class="p">(</span><span class="dl">"</span><span class="s2">key</span><span class="dl">"</span><span class="p">));</span> <span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="nx">keyValue</span><span class="p">.</span><span class="nf">zoom</span><span class="p">(</span><span class="nf">into</span><span class="p">(</span><span class="dl">"</span><span class="s2">value</span><span class="dl">"</span><span class="p">));</span> </code></pre> </div> <p><a href="proxy.php?url=https://github.com/igrep/svelte-store-tree/blob/30740be58afe4413f0d4e2d454bfe2b340a929e4/example/Tree.svelte#L13-L15" rel="noopener noreferrer">See in GitHub</a></p> <p>Finally, we can two-way bind the <code>key</code> and <code>value</code> returned by <code>zoom</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight html"><code><span class="nt">&lt;dl&gt;</span> <span class="nt">&lt;dt&gt;&lt;input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">bind:value=</span><span class="s">{$key}</span> <span class="nt">/&gt;&lt;/dt&gt;</span> <span class="nt">&lt;dd&gt;&lt;input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">bind:value=</span><span class="s">{$value}</span> <span class="nt">/&gt;&lt;/dd&gt;</span> <span class="nt">&lt;/dl&gt;</span> </code></pre> </div> <p><a href="proxy.php?url=https://github.com/igrep/svelte-store-tree/blob/30740be58afe4413f0d4e2d454bfe2b340a929e4/example/Tree.svelte#L68-L71" rel="noopener noreferrer">See in GitHub</a></p> <h4> Parents can react to updates by their children </h4> <p>All examples so far doesn't actually need svelte-store-tree. Just managing the state in each component would suffice instead of managing the state as a single large structure. Needless to say, svelte-store-tree does more: it can <strong>propagate updates in the children only to their direct parents</strong>.</p> <p>For example, assume that the <code>&lt;input&gt;</code> bound with the <code>key</code> from the last example got new input.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight html"><code><span class="nt">&lt;dl&gt;</span> <span class="c">&lt;!--------- THIS &lt;input&gt; -------------&gt;</span> <span class="nt">&lt;dt&gt;&lt;input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">bind:value=</span><span class="s">{$key}</span> <span class="nt">/&gt;&lt;/dt&gt;</span> <span class="nt">&lt;dd&gt;&lt;input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">bind:value=</span><span class="s">{$value}</span> <span class="nt">/&gt;&lt;/dd&gt;</span> <span class="nt">&lt;/dl&gt;</span> </code></pre> </div> <p>Then, the value <code>set</code> to <code>key</code> is conveyed to the functions <code>subscribe</code>ing the <code>key</code> itself, or <code>key</code>'s direct ancestors including <code>keyValue</code>. Functions <code>subscribe</code>ing sibling stores such as <code>value</code> or the parents' siblings, don't see the update.</p> <p>To illustrate, imagine the tree were shaped as follows:</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F265na2gif5ivtcxxv28z.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F265na2gif5ivtcxxv28z.png" title="Example tree to update one of its children" alt="Tree whose root is List 1, and the children are List 2, string and KeyValue"></a></p> <p>When updating the Key, the update is propagated to the three stores: List 1, KeyValue, and Key.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7uddrpfdctvt75ow7f4d.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7uddrpfdctvt75ow7f4d.png" title="Where the update is propagated when updating the Key" alt="Update the tree whose root is List 1, and the children are List 2, string and KeyValue"></a></p> <p>The Value, string, List 2, and List 2's children don't know the update. Thus, Svelte can avoid extra rerenderings.</p> <p>To demonstrate that feature, the example app sums up the number of the nodes in the tree by their type:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight html"><code><span class="nt">&lt;script </span><span class="na">lang=</span><span class="s">"ts"</span><span class="nt">&gt;</span> <span class="k">import</span> <span class="nx">TreeComponent</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./Tree.svelte</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">tree</span><span class="p">,</span> <span class="nx">type</span> <span class="nx">NodeType</span><span class="p">,</span> <span class="nx">type</span> <span class="nx">Tree</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./tree</span><span class="dl">"</span><span class="p">;</span> <span class="nl">$</span><span class="p">:</span> <span class="nx">stats</span> <span class="o">=</span> <span class="nf">countByNode</span><span class="p">(</span><span class="nx">$tree</span><span class="p">);</span> <span class="kd">function</span> <span class="nf">countByNode</span><span class="p">(</span><span class="nx">node</span><span class="p">:</span> <span class="nx">Tree</span><span class="p">):</span> <span class="nb">Map</span><span class="o">&lt;</span><span class="nx">NodeType</span><span class="p">,</span> <span class="nx">number</span><span class="o">&gt;</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span> <span class="nt">&lt;/script&gt;</span> <span class="nt">&lt;table&gt;</span> <span class="nt">&lt;tr&gt;</span> <span class="nt">&lt;th&gt;</span>Node Type<span class="nt">&lt;/th&gt;</span> <span class="nt">&lt;th&gt;</span>Count<span class="nt">&lt;/th&gt;</span> <span class="nt">&lt;/tr&gt;</span> {#each Array.from(stats) as [nodeType, count]} <span class="nt">&lt;tr&gt;</span> <span class="nt">&lt;td&gt;</span>{nodeType}<span class="nt">&lt;/td&gt;</span> <span class="nt">&lt;td&gt;</span>{count}<span class="nt">&lt;/td&gt;</span> <span class="nt">&lt;/tr&gt;</span> {/each} <span class="nt">&lt;/table&gt;</span> ... </code></pre> </div> <p><a href="proxy.php?url=https://github.com/igrep/svelte-store-tree/blob/30740be58afe4413f0d4e2d454bfe2b340a929e4/example/Root.svelte" rel="noopener noreferrer">See in GitHub</a></p> <h2> V.S. Solid.js's Store </h2> <p>As I wrote in the beginning of this article, I referred <a href="proxy.php?url=https://www.solidjs.com/tutorial/stores_createstore" rel="noopener noreferrer">Solid.js's store</a> feature in developing svelte-store-tree v0.3.1 because it's made for the similar goal with svelte-store-tree. From now on, let me explain in what way svelte-store-tree is similar to Solid.js's store, and what feature of Solid.js's store it failed to support because of the limitation of Svelte's store, respectively. I hope this would be a hint for discussion on the future design of Svelte.</p> <h3> Quick introduction to Solid.js's Store </h3> <p>Solid.js's store, as the page I referred in the last section described as "Solid's answer to nested reactivity", enables us to update a part of nested structures and track the part of the state. The <code>createStore</code> function returns a pair of the current store value, and the function to update the store value (slightly similar to React's <code>useState</code>):</p> <p>ℹ️ All examples in this section are quoted from <a href="proxy.php?url=https://www.solidjs.com/docs/latest/api#updating-stores" rel="noopener noreferrer">the official web site</a>.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="kd">const</span> <span class="p">[</span><span class="nx">state</span><span class="p">,</span> <span class="nx">setState</span><span class="p">]</span> <span class="o">=</span> <span class="nf">createStore</span><span class="p">({</span> <span class="na">todos</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="na">task</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Finish work</span><span class="dl">'</span><span class="p">,</span> <span class="na">completed</span><span class="p">:</span> <span class="kc">false</span> <span class="p">},</span> <span class="p">{</span> <span class="na">task</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Go grocery shopping</span><span class="dl">'</span><span class="p">,</span> <span class="na">completed</span><span class="p">:</span> <span class="kc">false</span> <span class="p">},</span> <span class="p">{</span> <span class="na">task</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Make dinner</span><span class="dl">'</span><span class="p">,</span> <span class="na">completed</span><span class="p">:</span> <span class="kc">false</span> <span class="p">},</span> <span class="p">]</span> <span class="p">});</span> </code></pre> </div> <p>The first thing of the pair, <code>state</code> is wrapped by a <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy" rel="noopener noreferrer">Proxy</a> so that the runtime can track access to the value of the store including its properties.</p> <p>Using the second value of the pair <code>setState</code>, we can specify "path" objects, such as names of the properties and predicate functions to decide which elements to update (if the value is an array), to tell which part of the store value should be updated. For example:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="c1">// Set true to the `completed` property of the 0th and 2nd elements in the `todos` property.</span> <span class="nf">setState</span><span class="p">(</span><span class="dl">'</span><span class="s1">todos</span><span class="dl">'</span><span class="p">,</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">2</span><span class="p">],</span> <span class="dl">'</span><span class="s1">completed</span><span class="dl">'</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span> <span class="c1">// Append `'!'` to the `task` property of `todos` elements whose `completed` property is `true`.</span> <span class="nf">setState</span><span class="p">(</span><span class="dl">'</span><span class="s1">todos</span><span class="dl">'</span><span class="p">,</span> <span class="nx">todo</span> <span class="o">=&gt;</span> <span class="nx">todo</span><span class="p">.</span><span class="nx">completed</span><span class="p">,</span> <span class="dl">'</span><span class="s1">task</span><span class="dl">'</span><span class="p">,</span> <span class="nx">t</span> <span class="o">=&gt;</span> <span class="nx">t</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">!</span><span class="dl">'</span><span class="p">);</span> </code></pre> </div> <p>The <code>setState</code> is so powerful that it can update arbitrary depth of the nested structure without any other library functions.</p> <p>In addition, as introduced before, Solid.js's store is designed for separately using the <code>Proxy</code>-wrapped value and the function to update, while Svelte's store is designed for using as a single object with <code>set</code> and <code>subscribe</code> to make it available for two-way binding.</p> <h3> Feature that affected svelte-store-tree </h3> <p>Somewhat affected by Solid.js's store, I improved the <code>Accessor</code> API of svelte-store-tree, used to show how to get deeper into the nested structure. Specifically, <code>svelte-store-tree</code> can now compose <code>Accessor</code> objects by their <code>and</code> method as the <code>setState</code> function of Solid.js (the second value of the <code>createStore</code>'s result) can compose the "path" objects given as its arguments.</p> <p>For example, by combining the <code>into</code> and <code>isPresent</code> <code>Accessor</code>, we can make a store that calls <code>subscribe</code>r functions if its <code>foo</code> property is not <code>undefined</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="nx">store</span><span class="p">.</span><span class="nf">zoom</span><span class="p">(</span><span class="nf">into</span><span class="p">(</span><span class="dl">'</span><span class="s1">foo</span><span class="dl">'</span><span class="p">).</span><span class="nf">and</span><span class="p">(</span><span class="nf">isPresent</span><span class="p">()));</span> </code></pre> </div> <p>Thanks to them, the code to obtain the <code>key</code> property from the <code>tree</code> in the example app can be rewritten like below:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">tree</span><span class="p">.</span><span class="nf">zoom</span><span class="p">(</span><span class="nf">choose</span><span class="p">(</span><span class="nx">chooseKeyValue</span><span class="p">).</span><span class="nf">and</span><span class="p">(</span><span class="nf">into</span><span class="p">(</span><span class="dl">"</span><span class="s2">key</span><span class="dl">"</span><span class="p">)));</span> </code></pre> </div> <p>Why don't I define the <code>zoom</code> method to receive multiple <code>Accessor</code>s to compose? One of the reasons is to simplify the implementation of <code>zoom</code>, and another is that it's not a good idea to pile up many <code>Accessor</code>s at once to access to too deep part of the nested structure, in my opinion.</p> <p>In detail, the former means that the code of <code>zoom</code> is more concise because it doesn't have to take care of multiple <code>Accessor</code>s. Composing <code>Accessor</code>s is just the business of the <code>and</code> method.</p> <p>And the latter is a design issue. Diving several levels down into a nested structure at a time is like surgery for the internal: it incurs a risk that the code gets vulnerable to change. Besides, composing <code>Accessor</code>s by listing them in the arguments can't work well for recursively nested structures, which I suppose to be a primary use case of svelte-store-tree. Because their depth varies dynamically.</p> <p>I determined this design based on the usage I assumed, at the sacrifice of verbosity of composing the <code>and</code> methods.</p> <h3> Feature that svelte-store-tree failed to support </h3> <p>I made svelte-store-tree aim for "the Solid.js's store for Svelte". But I couldn't reproduce one of its features anyway: Solid.js does NOT need a feature equivalent to svelte-store-tree's <code>choose</code> function<sup id="fnref3">3</sup>. I wasted a section for describing <code>choose</code> in a way!</p> <p>Why doesn't Solid.js's store require a feature like <code>choose</code>? That is related to its design that the object to read the store value and the function to update the store value are separated.</p> <p>Solid.js's store can select the value only by branching in the component on its value using <a href="proxy.php?url=https://www.solidjs.com/tutorial/flow_show" rel="noopener noreferrer"><code>Show</code></a> or <a href="proxy.php?url=https://www.solidjs.com/tutorial/flow_switch" rel="noopener noreferrer"><code>Switch</code> / <code>Match</code></a> (<code>if</code> statements in Solid.js).</p> <p>The same seems appied to svelte-store-tree, but that isn't true. Recall the first example of <code>choose</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight html"><code><span class="nt">&lt;script </span><span class="na">lang=</span><span class="s">"ts"</span><span class="nt">&gt;</span> <span class="c1">// ...</span> <span class="kd">const</span> <span class="nx">keyValue</span> <span class="o">=</span> <span class="nx">tree</span><span class="p">.</span><span class="nf">zoom</span><span class="p">(</span><span class="nf">choose</span><span class="p">(</span><span class="nx">chooseKeyValue</span><span class="p">));</span> <span class="kd">const</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">keyValue</span><span class="p">.</span><span class="nf">zoom</span><span class="p">(</span><span class="nf">into</span><span class="p">(</span><span class="dl">"</span><span class="s2">key</span><span class="dl">"</span><span class="p">));</span> <span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="nx">keyValue</span><span class="p">.</span><span class="nf">zoom</span><span class="p">(</span><span class="nf">into</span><span class="p">(</span><span class="dl">"</span><span class="s2">value</span><span class="dl">"</span><span class="p">));</span> <span class="c1">// ...</span> <span class="nt">&lt;/script&gt;</span> {#if typeof $tree === "string"} ... {:else if $tree === undefined} ... {:else if $tree instanceof Array} ... {:else} ... <span class="nt">&lt;dt&gt;&lt;input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">bind:value=</span><span class="s">{$key}</span> <span class="nt">/&gt;&lt;/dt&gt;</span> <span class="nt">&lt;dd&gt;&lt;input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">bind:value=</span><span class="s">{$value}</span> <span class="nt">/&gt;&lt;/dd&gt;</span> ... {/if} </code></pre> </div> <p>The two stores <code>key</code> and <code>value</code> are used only when the value of <code>tree</code> is neither <code>string</code>, <code>undefined</code>, nor <code>Array</code>, but <code>KeyValue</code>, as it's chosen by the <code>{#if} ... {/if}</code>. Getting <code>key</code> and <code>value</code> via <code>choose(chooseKeyValue)</code> is actually repeating the narrowing-down by the <code>{#if} ... {/if}</code>.</p> <p>So you might try modify the component like following with <code>{@const ...}</code> after <code>key</code> and <code>value</code> are used only in the <code>{:else}</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight html"><code>{:else} ... {@const key = tree.zoom(into("key"))} {@const value = tree.zoom(into("value"))} <span class="nt">&lt;dt&gt;&lt;input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">bind:value=</span><span class="s">{$key}</span> <span class="nt">/&gt;&lt;/dt&gt;</span> <span class="nt">&lt;dd&gt;&lt;input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">bind:value=</span><span class="s">{$value}</span> <span class="nt">/&gt;&lt;/dd&gt;</span> ... {/if} </code></pre> </div> <p>Though svelte-check raises an error about the <code>{@const ...}</code> below:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>/svelte-store-tree/example/Tree.svelte:69:42 Error: Stores must be declared at the top level of the component (this may change in a future version of Svelte) (svelte) </code></pre> </div> <p>It says that stores must be declared at the top-level<sup id="fnref4">4</sup>, thus you can't define a new store in a <code>{@const ...}</code> to <code>subscribe</code>. It's impossible to make a store only on a specific condition so far.</p> <p>To avoid that error forcibly, you have the option to split out a component containing only <code>&lt;input type="text" bind:value={$key} /&gt;</code> and <code>&lt;input type="text" bind:value={$value} /&gt;</code> then make the component take a <code>WritableTree&lt;KeyValue&gt;</code>. However, there is still a type error: the branching beginning with <code>{#if typeof $tree === "string"}</code> narrows only <code>$tree</code>, that is the current value of <code>tree</code>, so doesn't narrow the <code>tree</code> to <code>WritableTree&lt;KeyValue&gt;</code> from <code>WritableTree&lt;Tree&gt;</code>. TypeScript doesn't take that into consideration.</p> <p>Due to the problem I showed here, I gave up adding the feature following the exisiting convention of the store of Svelte.</p> <p>Why do we have to narrow the store itself as well as its value in Svelte? Because Svelte assigns both the API to read the store value (<code>subscribe</code>) and the API to update (<code>set</code> etc.) to the same single object. I'll simplify <code>WritableTree</code> to explain the detail:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="nx">WritableTree</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="c1">// Get the current value of the store.</span> <span class="c1">// I replaced the `subscribe` method with this for simplicity.</span> <span class="na">get</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">T</span><span class="p">;</span> <span class="c1">// Set a new value of the store. This is same as the original `set`.</span> <span class="nl">set</span><span class="p">:</span> <span class="p">(</span><span class="na">newValue</span><span class="p">:</span> <span class="nx">T</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">void</span><span class="p">;</span> <span class="p">};</span> </code></pre> </div> <p>Let's give a concrete type such as <code>number | undefined</code> to <code>WritableTree</code> to instantiate a store whose value can contain <code>undefined</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="nx">WritableTree</span><span class="o">&lt;</span><span class="kr">number</span> <span class="o">|</span> <span class="kc">undefined</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">get</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kr">number</span> <span class="o">|</span> <span class="kc">undefined</span><span class="p">;</span> <span class="nl">set</span><span class="p">:</span> <span class="p">(</span><span class="na">newValue</span><span class="p">:</span> <span class="kr">number</span> <span class="o">|</span> <span class="kc">undefined</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">void</span><span class="p">;</span> <span class="p">};</span> </code></pre> </div> <p>Now, let's say we want to remove the <code>undefined</code> to convert it to <code>WritableTree&lt;number&gt;</code>. Code to use the <code>get</code> method of <code>WritableTree&lt;number&gt;</code> doesn't expect <code>get</code> to return <code>undefined</code>, consequently we have to make it return only <code>number</code>. Meanwhile code to write a <code>number</code> on <code>WritableTree&lt;number&gt;</code> just passes <code>number</code>s to the <code>set</code> method, then we can reuse the function <code>(newValue: number | undefined) =&gt; void</code> as is<sup id="fnref5">5</sup>.</p> <p>Hence, what we have to narrow is only the API to read the store value in fact. While Solid.js's store just has to narrow the current store value by branching, svelte-store-tree has to narrow down both the functions because it forces a single object to contain both the method to read and the method to set.</p> <ol> <li id="fn1"> <p>Technically speaking, I implemented svelte-store-tree by rewriting the code of the store after copy-and-pasting it. So it doesn't <em>add</em> the methods. ↩</p> </li> <li id="fn2"> <p>"zoom", "choose", and "refuse" rhyme. ↩</p> </li> <li id="fn3"> <p>The feature to filter the store value with predicate functions in Solid.js, sounds similar to <code>choose</code>, but it's available only when the store value is an array. Note that it's not for narrowing types as <code>choose</code> does. ↩</p> </li> <li id="fn4"> <p>The top-level here seems to mean the top in the <code>&lt;script&gt;</code> block of the component. ↩</p> </li> <li id="fn5"> <p>This difference is widely known as "covariant and contravariant". The Japanese book <a href="proxy.php?url=https://gihyo.jp/book/2022/978-4-297-12747-3" rel="noopener noreferrer">"Introduction to TypeScript for Those Who Wants to Be a Pro" (プロを目指す人のためのTypeScript入門)</a> also explains. ↩</p> </li> </ol> typescript svelte solidjs statemanagement Divergent States in a "Single Source of Truth" Framework YAMAMOTO Yuji Thu, 07 Apr 2022 10:02:34 +0000 https://dev.to/igrep/divergent-states-in-a-single-source-of-truth-framework-1fh5 https://dev.to/igrep/divergent-states-in-a-single-source-of-truth-framework-1fh5 <p>I'll tell you what I've learnt from struggling with a bug that made me lose a couple of weeks. The application framework used in this post is <a href="proxy.php?url=https://github.com/jorgebucaran/hyperapp/">Hyperapp</a>, but I guess the same problem can be found in frameworks based on transforming the state of "Single Source of Truth" with pure functions (such as Elm, Redux, so on) if we use them in a wrong way.</p> <h2> Introduction to Hyperapp with an Example App without the Bug </h2> <p><a href="proxy.php?url=https://github.com/jorgebucaran/hyperapp/">Hyperapp</a> is a minimalistic framework that enables us to create virtual DOM based apps with architecture similar to <a href="proxy.php?url=https://guide.elm-lang.org/architecture/">The Elm Architecture</a> (TEA). Hyperapp gives us both a view framework based on virtual DOM and a Redux-like state management framework without learning Elm with its smaller-than-favicon size.</p> <p>Here is an example app of Hyperapp written in TypeScript<sup id="fnref1">1</sup>. I'll use this app to demonstrate the problem. So it's much simpler than the app I'm actually developing, but may look too complicated as an example of Hyperapp. Sorry!</p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">buildInit</span> <span class="o">=</span> <span class="p">():</span> <span class="nx">State</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Map</span><span class="p">();</span> <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">3</span><span class="p">;</span> <span class="o">++</span><span class="nx">i</span><span class="p">){</span> <span class="kd">const</span> <span class="nx">key1</span> <span class="o">=</span> <span class="nx">randomKey</span><span class="p">();</span> <span class="nx">result</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">key1</span><span class="p">,</span> <span class="p">{});</span> <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="mi">3</span><span class="p">;</span> <span class="o">++</span><span class="nx">j</span><span class="p">){</span> <span class="kd">const</span> <span class="nx">key2</span> <span class="o">=</span> <span class="nx">randomKey</span><span class="p">();</span> <span class="nx">result</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">key1</span><span class="p">)[</span><span class="nx">key2</span><span class="p">]</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="k">return</span> <span class="nx">result</span><span class="p">;</span> <span class="p">};</span> <span class="c1">// Action functions to update the State object</span> <span class="kd">const</span> <span class="nx">SetTrue</span> <span class="o">=</span> <span class="p">(</span><span class="nx">state</span><span class="p">:</span> <span class="nx">State</span><span class="p">,</span> <span class="p">[</span><span class="nx">key1</span><span class="p">,</span> <span class="nx">key2</span><span class="p">]:</span> <span class="p">[</span><span class="kr">string</span><span class="p">,</span> <span class="kr">string</span><span class="p">]):</span> <span class="nx">State</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">newState</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Map</span><span class="p">(</span><span class="nx">state</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">childState</span> <span class="o">=</span> <span class="nx">newState</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">key1</span><span class="p">);</span> <span class="nx">newState</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">key1</span><span class="p">,</span> <span class="p">{</span> <span class="p">...</span><span class="nx">childState</span><span class="p">,</span> <span class="p">[</span><span class="nx">key2</span><span class="p">]:</span> <span class="kc">true</span><span class="p">,</span> <span class="p">});</span> <span class="k">return</span> <span class="nx">newState</span><span class="p">;</span> <span class="p">};</span> <span class="kd">const</span> <span class="nx">SetFalse</span> <span class="o">=</span> <span class="p">(</span><span class="nx">state</span><span class="p">:</span> <span class="nx">State</span><span class="p">,</span> <span class="p">[</span><span class="nx">key1</span><span class="p">,</span> <span class="nx">key2</span><span class="p">]:</span> <span class="p">[</span><span class="kr">string</span><span class="p">,</span> <span class="kr">string</span><span class="p">]):</span> <span class="nx">State</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">newState</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Map</span><span class="p">(</span><span class="nx">state</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">childState</span> <span class="o">=</span> <span class="nx">newState</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">key1</span><span class="p">);</span> <span class="nx">newState</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">key1</span><span class="p">,</span> <span class="p">{</span> <span class="p">...</span><span class="nx">childState</span><span class="p">,</span> <span class="p">[</span><span class="nx">key2</span><span class="p">]:</span> <span class="kc">false</span><span class="p">,</span> <span class="p">});</span> <span class="k">return</span> <span class="nx">newState</span><span class="p">;</span> <span class="p">};</span> <span class="kd">const</span> <span class="nx">view</span> <span class="o">=</span> <span class="p">(</span><span class="nx">state</span><span class="p">:</span> <span class="nx">State</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">children1</span> <span class="o">=</span> <span class="p">[];</span> <span class="c1">// Side note: I should use the `map` method here, but I couldn't find</span> <span class="c1">// it's available just until I write up most of this post....</span> <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="p">[</span><span class="nx">key1</span><span class="p">,</span> <span class="nx">childState</span><span class="p">]</span> <span class="k">of</span> <span class="nx">state</span><span class="p">.</span><span class="nx">entries</span><span class="p">())</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">children2</span> <span class="o">=</span> <span class="p">[];</span> <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">key2</span> <span class="k">of</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">childState</span><span class="p">))</span> <span class="p">{</span> <span class="nx">children2</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span> <span class="nx">h</span><span class="p">(</span> <span class="dl">"</span><span class="s2">span</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">onmousemove</span><span class="p">:</span> <span class="p">[</span><span class="nx">SetTrue</span><span class="p">,</span> <span class="p">[</span><span class="nx">key1</span><span class="p">,</span> <span class="nx">key2</span><span class="p">]],</span> <span class="na">onmouseleave</span><span class="p">:</span> <span class="p">[</span><span class="nx">SetFalse</span><span class="p">,</span> <span class="p">[</span><span class="nx">key1</span><span class="p">,</span> <span class="nx">key2</span><span class="p">]],</span> <span class="na">class</span><span class="p">:</span> <span class="p">{</span> <span class="na">elem</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">true</span><span class="p">:</span> <span class="nx">childState</span><span class="p">[</span><span class="nx">key2</span><span class="p">]</span> <span class="p">},</span> <span class="p">},</span> <span class="nx">text</span><span class="p">(</span><span class="nx">key2</span><span class="p">),</span> <span class="p">)</span> <span class="p">);</span> <span class="p">}</span> <span class="nx">children1</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span> <span class="nx">h</span><span class="p">(</span> <span class="dl">"</span><span class="s2">div</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">class</span><span class="p">:</span> <span class="dl">"</span><span class="s2">elem</span><span class="dl">"</span> <span class="p">},</span> <span class="p">[</span> <span class="nx">text</span><span class="p">(</span><span class="nx">key1</span><span class="p">),</span> <span class="p">...</span> <span class="nx">children2</span><span class="p">,</span> <span class="p">]</span> <span class="p">)</span> <span class="p">);</span> <span class="p">}</span> <span class="k">return</span> <span class="nx">h</span><span class="p">(</span><span class="dl">"</span><span class="s2">div</span><span class="dl">"</span><span class="p">,</span> <span class="p">{},</span> <span class="nx">children1</span><span class="p">);</span> <span class="p">};</span> <span class="c1">// Run the app</span> <span class="nx">app</span><span class="p">({</span> <span class="na">init</span><span class="p">:</span> <span class="nx">buildInit</span><span class="p">(),</span> <span class="na">view</span><span class="p">:</span> <span class="nx">view</span><span class="p">,</span> <span class="na">node</span><span class="p">:</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">app</span><span class="dl">"</span><span class="p">),</span> <span class="p">});</span> </code></pre> </div> <p>First of all, a basic application in Hyperapp like this is split into three parts: State, View, and Action. Each of them corresponds to Model, View, Update of TEA:</p> <ul> <li>State: The application's internal state. Updated by Actions.</li> <li>View: Function which takes a State to return a virtual DOM object.</li> <li>Action: Function which takes a State to return a new State.</li> </ul> <p>Like a View in TEA generates messages from its event handlers (event handlers set in the virtual DOM returned by the View), a View in Hyperapp dispatches Actions from its event handlers. This is a code to show how event handlers are set in the View, excerpted from the example <code>view</code> function above:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="nx">h</span><span class="p">(</span> <span class="dl">'</span><span class="s1">span</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">onmousemove</span><span class="p">:</span> <span class="p">[</span><span class="nx">SetTrue</span><span class="p">,</span> <span class="p">[</span><span class="nx">key1</span><span class="p">,</span> <span class="nx">key2</span><span class="p">]],</span> <span class="na">onmouseleave</span><span class="p">:</span> <span class="p">[</span><span class="nx">SetFalse</span><span class="p">,</span> <span class="p">[</span><span class="nx">key1</span><span class="p">,</span> <span class="nx">key2</span><span class="p">]],</span> <span class="na">class</span><span class="p">:</span> <span class="p">{</span> <span class="na">elem</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">false</span><span class="p">:</span> <span class="nx">state</span><span class="p">[</span><span class="nx">key1</span><span class="p">][</span><span class="nx">key2</span><span class="p">]</span> <span class="p">},</span> <span class="p">},</span> <span class="nx">text</span><span class="p">(</span><span class="nx">key2</span><span class="p">)</span> <span class="p">)</span> </code></pre> </div> <p>The <code>h</code> function is the construtor of a virtual DOM object in Hyperapp, which takes the name of the tag (here it's <code>span</code>), attributes of the element as an object, and the element's child node(s) (here it's <code>text(key2)</code>). Actions dispatched by the event handlers are set in the second argument's <code>on***</code> attributes, along with their extra argument (called "payload"). In the extracted code, the Action <code>SetTrue</code> with the payload <code>[key1, key2]</code> is dispatched by a <code>mousemove</code> event, and <code>SetFalse</code> with <code>[key1, key2]</code> is dispatched by a <code>mouseleave</code>.</p> <p>What happens after <code>SetTrue</code> or <code>SetFalse</code> gets dispatched? Hyperapp calls the dispatched Action with the current State, its payload, and the event object created by the <code>mousemove</code>/<code>mouseleave</code> event (but the event object is not used in the example!). Then, it passes the State returned by the Action to the View to get the refreshed virtual DOM to update the actual DOM tree. And the (re)rendered DOM tree again waits for new events to dispatch Actions.</p> <p>Okay, those are the important points of Hyperapp that I want you to know before explaining the problem.</p> <h2> Compare the Behavior of the Buggy App and Non-Buggy One </h2> <p>Now, let's see how the example app works and how it gets broken by the mistake I made. I created a StackBlitz project containing both one without the bug and one with the bug in a single HTML file:</p> <p><a href="proxy.php?url=https://stackblitz.com/edit/typescript-befryd?file=index.ts">https://stackblitz.com/edit/typescript-befryd?file=index.ts</a></p> <p>And this is the screenshot of the working app:</p> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--zBTuzv8f--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l9io005x3lkz1bgh3twu.gif" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--zBTuzv8f--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l9io005x3lkz1bgh3twu.gif" alt="Only the child element where the mouse cursor is over gets blue." width="649" height="678"></a></p> <p>The example app is as simple as it receives <code>mouseover</code> events to paint the <code>&lt;span&gt;</code> element blue, then gets it back by <code>mouseleave</code> events.</p> <p>By contrast, the example app I injected the bug into doesn't restore the color of the <code>mouseleave</code>ed <code>&lt;span&gt;</code> element:</p> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--XkaBgGli--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lb4lbvj7ryg5pn661s1c.gif" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--XkaBgGli--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lb4lbvj7ryg5pn661s1c.gif" alt="Some of the elements are still blue even after the mouse cursor leaves." width="648" height="678"></a></p> <h3> The Change that Caused the Problem </h3> <p>What's the difference between the buggy example and the non-buggy example? Before diggging into the details, let me explain the motive for the change. The function that made me anxious is the <code>SetTrue</code> Action (and <code>SetFalse</code> did too):<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">SetTrue</span> <span class="o">=</span> <span class="p">(</span><span class="nx">state</span><span class="p">:</span> <span class="nx">State</span><span class="p">,</span> <span class="p">[</span><span class="nx">key1</span><span class="p">,</span> <span class="nx">key2</span><span class="p">]:</span> <span class="p">[</span><span class="nx">string</span><span class="p">,</span> <span class="nx">string</span><span class="p">]):</span> <span class="nx">State</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">newState</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Map</span><span class="p">(</span><span class="nx">state</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">childState</span> <span class="o">=</span> <span class="nx">newState</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">key1</span><span class="p">);</span> <span class="nx">newState</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">key1</span><span class="p">,</span> <span class="p">{</span> <span class="p">...</span><span class="nx">childState</span><span class="p">,</span> <span class="p">[</span><span class="nx">key2</span><span class="p">]:</span> <span class="kc">true</span><span class="p">,</span> <span class="p">});</span> <span class="k">return</span> <span class="nx">newState</span><span class="p">;</span> <span class="p">};</span> </code></pre> </div> <p>The expression <code>newState.get(key1)</code> may return <code>undefined</code> since <code>newState</code> is a <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map"><code>Map</code></a> in the standard library and its <code>get</code> method returns <code>undefined</code> if the <code>Map</code> doesn't contain the associated value. TypeScript doesn't complain about this because <code>{ ...undefined }</code> returns <code>{}</code> without a runtime error!, but catching <code>undefined</code> here is not expected. And in my actual app, it isn't evident that the <code>get</code> method always returns a non-undefined value.</p> <p>Checking if the result of <code>newState.get(key1)</code> is <code>undefined</code> or not is trivial enough, but looking out over the <code>view</code> and <code>SetTrue</code>/<code>SetFalse</code> functions, you will find that the value equivalent to <code>newState.get(key1)</code>, <code>childState</code>, is available as the loop variable of the outermost <code>for ... of</code> statement in <code>view</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">view</span> <span class="o">=</span> <span class="p">(</span><span class="nx">state</span><span class="p">:</span> <span class="nx">State</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="p">[</span><span class="nx">key1</span><span class="p">,</span> <span class="nx">childState</span><span class="p">]</span> <span class="k">of</span> <span class="nx">state</span><span class="p">.</span><span class="nx">entries</span><span class="p">())</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="c1">// ...</span> <span class="p">};</span> </code></pre> </div> <p>That's why I decided to pass <code>childState</code> as one of the payload of <code>SetTrue</code>/<code>SetFalse</code> Action. I modified them as following:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">SetTrue</span> <span class="o">=</span> <span class="p">(</span> <span class="nx">state</span><span class="p">:</span> <span class="nx">State</span><span class="p">,</span> <span class="p">[</span><span class="nx">childState</span><span class="p">,</span> <span class="nx">key1</span><span class="p">,</span> <span class="nx">key2</span><span class="p">]:</span> <span class="p">[</span><span class="nx">ChildState</span><span class="p">,</span> <span class="kr">string</span><span class="p">,</span> <span class="kr">string</span><span class="p">]</span> <span class="p">):</span> <span class="nx">State</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">newState</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Map</span><span class="p">(</span><span class="nx">state</span><span class="p">);</span> <span class="nx">newState</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">key1</span><span class="p">,</span> <span class="p">{</span> <span class="p">...</span><span class="nx">childState</span><span class="p">,</span> <span class="p">[</span><span class="nx">key2</span><span class="p">]:</span> <span class="kc">true</span><span class="p">,</span> <span class="p">});</span> <span class="k">return</span> <span class="nx">newState</span><span class="p">;</span> <span class="p">};</span> </code></pre> </div> <p>Note that the line <code>const childState = newState.get(key1);</code> is removed, then the local variable <code>childState</code> is defined as the part of the second argument instead. Now the <code>view</code> function gives the <code>childState</code> loop variable to <code>SetTrue</code>/<code>SetFalse</code> :<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">view</span> <span class="o">=</span> <span class="p">(</span><span class="nx">state</span><span class="p">:</span> <span class="nx">State</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="p">[</span><span class="nx">key1</span><span class="p">,</span> <span class="nx">childState</span><span class="p">]</span> <span class="k">of</span> <span class="nx">state</span><span class="p">.</span><span class="nx">entries</span><span class="p">())</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="p">{</span> <span class="na">onmousemove</span><span class="p">:</span> <span class="p">[</span><span class="nx">SetTrue</span><span class="p">,</span> <span class="p">[</span><span class="nx">childState</span><span class="p">,</span> <span class="nx">key1</span><span class="p">,</span> <span class="nx">key2</span><span class="p">]],</span> <span class="na">onmouseleave</span><span class="p">:</span> <span class="p">[</span><span class="nx">SetFalse</span><span class="p">,</span> <span class="p">[</span><span class="nx">childState</span><span class="p">,</span> <span class="nx">key1</span><span class="p">,</span> <span class="nx">key2</span><span class="p">]],</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="c1">// ...</span> <span class="p">};</span> </code></pre> </div> <p>ℹ️ In the StackBlitz project I showed before, the <code>view</code>, <code>SetTrue</code> and <code>SetFalse</code> functions containing theses changes are renamed into <code>viewBuggy</code>, <code>SetTrueBuggy</code>, and <code>SetFalseBuggy</code>, respectively.</p> <p>These changes freed <code>SetTrue</code>/<code>SetFalse</code> from <code>undefined</code>-checking for every child of the state. That would improve the app's performance a little (But too little to see. Don't be obcessed with that!).</p> <p>Unfortunately, this is the very trigger of the weird behavior! The application won't handle the <code>mouseleave</code> event correctly anymore. It leaves the <code>mouseleave</code>ed element blue if the mouse cursor moves onto another element which also reacts to the <code>mousemove</code> event.</p> <h2> What Hyperapp does After a DOM Event Occurs </h2> <p>Why does the change spoil the app? Learn how Hyperapp updates the state first of all to get the answer. According to <a href="proxy.php?url=https://github.com/jorgebucaran/hyperapp/blob/876d3f44d0ae601de181714c583a07d83d3472c9/index.js">the source code</a>, Hyperapp handles DOM events as follows:</p> <ul> <li>(1) Call the Action assigned to the event with the state, the payload, and event object. <ul> <li>In the case of this article's app, the Action is <code>SetTrue</code> or <code>SetFalse</code>, and the payload is <code>[key1, key2]</code> or <code>[childState, key1, key2]</code>.</li> </ul> </li> <li>(2) Update the internal variable named <code>state</code> if the Action returns a state different from the original <code>state</code> (compared by <code>!==</code>). <ul> <li>Do nothing and stop the event handler if the returned state has no change.</li> </ul> </li> <li>(3) Unless the <code>render</code> function (introduce later) is still running, enqueue the <code>render</code> function by <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame"><code>requestAnimationFrame</code></a> (or <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/web/api/settimeout"><code>setTimeout</code></a> if <code>requestAnimationFrame</code> is unavailable).</li> <li>(4) The <code>render</code> function runs: call the <code>view</code> function with the updated <code>state</code> to produce renewed virtual DOM object, then compares each child of the old and new virtual DOM tree to patch the real DOM.</li> </ul> <p>This is sumarrized into the pseudo JavaScript code below:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="c1">// Variables updated while the app is alive</span> <span class="kd">let</span> <span class="nx">state</span><span class="p">,</span> <span class="nx">virtualDom</span><span class="p">,</span> <span class="nx">rendering</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">eventHandlers</span> <span class="o">=</span> <span class="p">{</span> <span class="na">onmousemove</span><span class="p">:</span> <span class="p">[</span><span class="nx">SetTrue</span><span class="p">,</span> <span class="nx">payload</span><span class="p">],</span> <span class="na">onmouseleave</span><span class="p">:</span> <span class="p">[</span><span class="nx">SetFalse</span><span class="p">,</span> <span class="nx">payload</span><span class="p">],</span> <span class="p">};</span> <span class="c1">// (1)</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">action</span><span class="p">,</span> <span class="nx">payload</span><span class="p">]</span> <span class="o">=</span> <span class="nx">eventHandlers</span><span class="p">[</span><span class="nx">eventName</span><span class="p">];</span> <span class="kd">const</span> <span class="nx">newState</span> <span class="o">=</span> <span class="nx">action</span><span class="p">(</span><span class="nx">state</span><span class="p">,</span> <span class="nx">payload</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">state</span> <span class="o">!==</span> <span class="nx">newState</span><span class="p">){</span> <span class="c1">// (2)</span> <span class="nx">state</span> <span class="o">=</span> <span class="nx">newState</span><span class="p">;</span> <span class="c1">// (3)</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">rendering</span><span class="p">){</span> <span class="nx">rendering</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span> <span class="nx">enqueue</span><span class="p">(</span><span class="nx">render</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">// (4)</span> <span class="kd">function</span> <span class="nx">render</span><span class="p">(){</span> <span class="kd">const</span> <span class="nx">newVirtualDom</span> <span class="o">=</span> <span class="nx">view</span><span class="p">(</span><span class="nx">state</span><span class="p">);</span> <span class="nx">compareVirtualDomsToPatchTheRealDom</span><span class="p">(</span><span class="nx">virtualDom</span><span class="p">,</span> <span class="nx">newVirtualDom</span><span class="p">);</span> <span class="nx">virtualDom</span> <span class="o">=</span> <span class="nx">newVirtualDom</span><span class="p">;</span> <span class="nx">rendering</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span> </code></pre> </div> <p>Recall <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop">how browsers handle JavaScript tasks</a>. Task here is a function call associated with the event (by <code>addEventListener</code> etc). The browser keeps running the function until the call stack gets empty without any interruption. In respect to the event handler of Hyperapp, from (1) to (3) is recognized as a single task (if the <code>state</code> changes). Because (3) just calls <code>requestAnimationFrame</code> to put off calling the <code>render</code> function. What <code>requestAnimationFrame</code> does is only registering the given function as a new task to execute later, then finishes its business. So the task initiated by the event finishes after calling <code>requestAnimationFrame</code>. As (3) does, calling <code>requestAnimationFrame</code> and do nothing afterwards is a typical way to let the browser process another task. You will find that the browser treats <code>requestAnimationFrame</code> as a special function according to the call stack by setting a break point to step in to the <code>render</code> function:</p> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--Rk2oLaf8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aah8sucj9g7dxius22m8.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--Rk2oLaf8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aah8sucj9g7dxius22m8.png" alt="Example call stack" width="307" height="200"></a></p> <p>This is an example call stack in Microsoft Edge's DevTools<sup id="fnref2">2</sup>. This shows that <code>requestAnimationFrame</code> switched the task running. The replaced task doesn't techinacally share the call stack with the older one, but Edge's debugger artificially concatenates them for convinience.</p> <h3> Consecutive Events Update the State in Order </h3> <p>The flow I described in the last section can be interrupted between (3) and (4), if a new task is enqueued while Hyperapp is performing (1)-(3). Such interruptions unsurprisingly happen when a pair of events occur simultaneously like the example app. That interruptions are caused by say, <code>mouseover</code> after <code>mouseleave</code>, <code>focus</code> after <code>blur</code>, and so on. As long as your app's Actions <em>just</em> receive the state and return the updated one, there are no problem because (2) definitely updates the state before yielding the control to the subsequent event. When a couple of serial events take place, the pseudo JavaScript code is rewritten as this:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="c1">// ... omitted...</span> <span class="c1">// (1) `eventName` can be `mouseleave`, `blur` etc.</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">action</span><span class="p">,</span> <span class="nx">payload</span><span class="p">]</span> <span class="o">=</span> <span class="nx">eventHandlers</span><span class="p">[</span><span class="nx">eventName</span><span class="p">];</span> <span class="kd">const</span> <span class="nx">newState</span> <span class="o">=</span> <span class="nx">action</span><span class="p">(</span><span class="nx">state</span><span class="p">,</span> <span class="nx">payload</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">state</span> <span class="o">!==</span> <span class="nx">newState</span><span class="p">){</span> <span class="c1">// (2)</span> <span class="nx">state</span> <span class="o">=</span> <span class="nx">newState</span><span class="p">;</span> <span class="c1">// (3)</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">rendering</span><span class="p">){</span> <span class="nx">rendering</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span> <span class="c1">// (1'): (1) for another event.</span> <span class="c1">// `anotherEventName` can be `mouseenter`, `focus` etc.</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">action</span><span class="p">,</span> <span class="nx">payload</span><span class="p">]</span> <span class="o">=</span> <span class="nx">eventHandlers</span><span class="p">[</span><span class="nx">anotherEventName</span><span class="p">];</span> <span class="kd">const</span> <span class="nx">newState</span> <span class="o">=</span> <span class="nx">action</span><span class="p">(</span><span class="nx">state</span><span class="p">,</span> <span class="nx">payload</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">state</span> <span class="o">!==</span> <span class="nx">newState</span><span class="p">){</span> <span class="c1">// (2')</span> <span class="nx">state</span> <span class="o">=</span> <span class="nx">newState</span><span class="p">;</span> <span class="c1">// (3') is omitted because `rendering` here must be true.</span> <span class="p">}</span> <span class="c1">// Now, `render` renders `state` updated by (2').</span> <span class="c1">// So the `render`ed State is up-to-date even after a sequence of</span> <span class="c1">// simultaneous events are dispatched.</span> <span class="nx">render</span><span class="p">();</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">// (4)</span> <span class="kd">function</span> <span class="nx">render</span><span class="p">(){</span> <span class="c1">// ... omitted...</span> <span class="p">}</span> </code></pre> </div> <h2> State Diverges by Unexpected References in the View </h2> <p>The point in the previous pseudo code is that <code>state</code> doesn't get stale: State passed as the first argument to the View and any Actions is always up-to-date. They wouldn't refer the State before updated by older events. Thus Hyperapp achieves "State is the Single Source of the Truth" --- as far as the Actions and their payload use the State correctly.</p> <p>Now, review the example app's <code>view</code> function with the problem:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">view</span> <span class="o">=</span> <span class="p">(</span><span class="nx">state</span><span class="p">:</span> <span class="nx">State</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="p">[</span><span class="nx">key1</span><span class="p">,</span> <span class="nx">childState</span><span class="p">]</span> <span class="k">of</span> <span class="nx">state</span><span class="p">.</span><span class="nx">entries</span><span class="p">())</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="p">{</span> <span class="na">onmousemove</span><span class="p">:</span> <span class="p">[</span><span class="nx">SetTrue</span><span class="p">,</span> <span class="p">[</span><span class="nx">childState</span><span class="p">,</span> <span class="nx">key1</span><span class="p">,</span> <span class="nx">key2</span><span class="p">]],</span> <span class="na">onmouseleave</span><span class="p">:</span> <span class="p">[</span><span class="nx">SetFalse</span><span class="p">,</span> <span class="p">[</span><span class="nx">childState</span><span class="p">,</span> <span class="nx">key1</span><span class="p">,</span> <span class="nx">key2</span><span class="p">]],</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="c1">// ...</span> <span class="p">};</span> </code></pre> </div> <p>To make sure <code>childState</code> is available for the <code>SetTrue</code> and <code>SetFalse</code> Action, I put it in their payload. Payload is set as the value of <code>on***</code> attribute of the virtual DOM node with its Action. So <code>childState</code> remains unchanged until the <code>view</code> function is called with the updated <code>state</code>. That means Actions can take the State updated by the precedent event and one not-yet updated. The State has diverged!</p> <p>Let's see the details by revising the pseudo JavaScript:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="c1">// Variables updated while the app is alive</span> <span class="kd">let</span> <span class="nx">state</span><span class="p">,</span> <span class="nx">virtualDom</span><span class="p">,</span> <span class="nx">rendering</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span> <span class="c1">// Up until now, `eventHandlers` is defined as an independent variable for</span> <span class="c1">// simplicity. But it's actually set as properties of `virtualDom` by the</span> <span class="c1">// `view` function.</span> <span class="nx">virtualDom</span><span class="p">.</span><span class="nx">eventHandlers</span> <span class="o">=</span> <span class="p">{</span> <span class="na">onmousemove</span><span class="p">:</span> <span class="p">[</span><span class="nx">SetTrue</span><span class="p">,</span> <span class="nx">state</span><span class="p">.</span><span class="nx">childState</span><span class="p">],</span> <span class="na">onmouseleave</span><span class="p">:</span> <span class="p">[</span><span class="nx">SetFalse</span><span class="p">,</span> <span class="nx">state</span><span class="p">.</span><span class="nx">childState</span><span class="p">],</span> <span class="p">};</span> <span class="c1">// (1). `eventName` can be `mouseleave`, `blur` etc.</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">action</span><span class="p">,</span> <span class="nx">payload</span><span class="p">]</span> <span class="o">=</span> <span class="nx">eventHandlers</span><span class="p">[</span><span class="nx">eventName</span><span class="p">];</span> <span class="kd">const</span> <span class="nx">newState</span> <span class="o">=</span> <span class="nx">action</span><span class="p">(</span><span class="nx">state</span><span class="p">,</span> <span class="nx">payload</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">state</span> <span class="o">!==</span> <span class="nx">newState</span><span class="p">){</span> <span class="c1">// (2)</span> <span class="nx">state</span> <span class="o">=</span> <span class="nx">newState</span><span class="p">;</span> <span class="c1">// (3)</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">rendering</span><span class="p">){</span> <span class="nx">rendering</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span> <span class="c1">// (1') ⚠️`state` is already updated in (2), but `virtualDom` is not yet!</span> <span class="c1">// So `virtualDom.eventHandlers` with its payload aren't either!</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">action</span><span class="p">,</span> <span class="nx">payload</span><span class="p">]</span> <span class="o">=</span> <span class="nx">virtualDom</span><span class="p">.</span><span class="nx">eventHandlers</span><span class="p">[</span><span class="nx">anotherEventName</span><span class="p">];</span> <span class="kd">const</span> <span class="nx">newState</span> <span class="o">=</span> <span class="nx">action</span><span class="p">(</span><span class="nx">state</span><span class="p">,</span> <span class="nx">payload</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">state</span> <span class="o">!==</span> <span class="nx">newState</span><span class="p">){</span> <span class="c1">// (2')</span> <span class="nx">state</span> <span class="o">=</span> <span class="nx">newState</span><span class="p">;</span> <span class="c1">// (3') is omitted because `rendering` here must be true.</span> <span class="p">}</span> <span class="nx">render</span><span class="p">();</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">// (4)</span> <span class="kd">function</span> <span class="nx">render</span><span class="p">(){</span> <span class="kd">const</span> <span class="nx">newVirtualDom</span> <span class="o">=</span> <span class="nx">view</span><span class="p">(</span><span class="nx">state</span><span class="p">);</span> <span class="nx">compareVirtualDomsToPatchTheRealDom</span><span class="p">(</span><span class="nx">virtualDom</span><span class="p">,</span> <span class="nx">newVirtualDom</span><span class="p">);</span> <span class="c1">// `virtualDom.eventHandlers` are finally updated here. But too late!</span> <span class="nx">virtualDom</span> <span class="o">=</span> <span class="nx">newVirtualDom</span><span class="p">;</span> <span class="nx">rendering</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span> </code></pre> </div> <p>The virtual DOM object is updated by View, and the State is updated by an Action. Due to the difference in when they are called, the Action can process an outdated state left in the virtual DOM as payload.</p> <h2> Conclusion and Extra Remarks </h2> <ul> <li>Don't refer to (some part of) the State in Actions except as their first argument. <ul> <li>In addition to payload, we have to take care of free variables if we define Actions inside the View.</li> </ul> </li> <li>I suspect we might encounter the same problem in other frameworks with the similar architecture (e.g. React/Redux, Elm).</li> </ul> <ol> <li id="fn1"> <p>Hyperapp doesn't force you to use TypeScript of course. But I'll use TypeScript for easier description of the type of the state. ↩</p> </li> <li id="fn2"> <p>I'm afraid I failed to reproduce a call stack like this with the debugger of my favorite Firefox's DevTools. 😞 ↩</p> </li> </ol> javascript hyperapp