{
    "componentChunkName": "component---src-templates-blog-detail-tsx",
    "path": "/blog/typescript-union/",
    "result": {"data":{"site":{"siteMetadata":{"siteTitleShort":"Developer Portfolio"}},"markdownRemark":{"id":"d94e0790-726f-54e0-85d2-10382d97b8ca","excerpt":"The pipe character does a lot of work I want to go through how I actually use unions in React work: on props, on reducer actions, and in the places where the…","html":"<h2>The pipe character does a lot of work</h2>\n<p class=\"lead\">A union type is the smallest possible piece of TypeScript syntax and one of the easiest to get subtly wrong. You write a pipe between two types and the compiler starts asking questions you didn't expect.</p>\n<p>I want to go through how I actually use unions in React work: on props, on reducer actions, and in the places where the compiler stops helping and you have to give it a hand. There is also a small pile of things I got wrong for longer than I’d like to admit, so those get their own section at the end.</p>\n<h3>What a union actually is</h3>\n<p>A union says a value is one of several types, but not which one. You write it with a pipe:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">type</span> <span class=\"token class-name\">MyUnion</span> <span class=\"token operator\">=</span> <span class=\"token builtin\">number</span> <span class=\"token operator\">|</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p><code class=\"language-text\">MyUnion</code> holds either a number or a string. The important half of that sentence is the part people skip: until you prove which one it is, TypeScript will only let you use the members that are common to both. <code class=\"language-text\">.toFixed()</code> is off limits, <code class=\"language-text\">.toUpperCase()</code> is off limits, and <code class=\"language-text\">.toString()</code> is fine because both have it.</p>\n<p>Proving which one it is called narrowing, and it is most of the work.</p>\n<h4>Function parameters</h4>\n<p>The classic case is a parameter that accepts one thing or a list of them:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">const</span> <span class=\"token function-variable function\">greet</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span>names<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span> <span class=\"token operator\">|</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token builtin\">Array</span><span class=\"token punctuation\">.</span><span class=\"token function\">isArray</span><span class=\"token punctuation\">(</span>names<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    names<span class=\"token punctuation\">.</span><span class=\"token function\">forEach</span><span class=\"token punctuation\">(</span>name <span class=\"token operator\">=></span> <span class=\"token builtin\">console</span><span class=\"token punctuation\">.</span><span class=\"token function\">log</span><span class=\"token punctuation\">(</span><span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">Hello, </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>name<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\">!</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span> <span class=\"token keyword\">else</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token builtin\">console</span><span class=\"token punctuation\">.</span><span class=\"token function\">log</span><span class=\"token punctuation\">(</span><span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">Hello, </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>names<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\">!</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span></code></pre></div>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token function\">greet</span><span class=\"token punctuation\">(</span><span class=\"token string\">'Alice'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token comment\">// output: Hello, Alice!</span>\n\n<span class=\"token function\">greet</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">[</span><span class=\"token string\">'Bob'</span><span class=\"token punctuation\">,</span> <span class=\"token string\">'Charlie'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token comment\">// output: Hello, Bob! Hello, Charlie!</span></code></pre></div>\n<p>Inside the <code class=\"language-text\">if</code>, <code class=\"language-text\">names</code> is <code class=\"language-text\">string[]</code>. Inside the <code class=\"language-text\">else</code>, it’s <code class=\"language-text\">string</code>. <code class=\"language-text\">Array.isArray</code> is one of the checks TypeScript understands natively, along with <code class=\"language-text\">typeof</code>, <code class=\"language-text\">instanceof</code>, <code class=\"language-text\">in</code>, and a plain truthiness test.</p>\n<h4>Conditional rendering in React</h4>\n<p>Same idea, applied to props. A component that renders differently depending on what it was handed:</p>\n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token keyword\">type</span> <span class=\"token class-name\">DisplayProps</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span> value<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span> <span class=\"token operator\">|</span> <span class=\"token builtin\">string</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">const</span> Display<span class=\"token operator\">:</span> React<span class=\"token punctuation\">.</span><span class=\"token constant\">FC</span><span class=\"token operator\">&lt;</span>DisplayProps<span class=\"token operator\">></span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span> value <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">return</span> <span class=\"token punctuation\">(</span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n      </span><span class=\"token punctuation\">{</span><span class=\"token keyword\">typeof</span> value <span class=\"token operator\">===</span> <span class=\"token string\">'number'</span> <span class=\"token operator\">?</span> <span class=\"token punctuation\">(</span>\n        <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>p</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">Number: </span><span class=\"token punctuation\">{</span>value<span class=\"token punctuation\">}</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>p</span><span class=\"token punctuation\">></span></span>\n      <span class=\"token punctuation\">)</span> <span class=\"token operator\">:</span> <span class=\"token punctuation\">(</span>\n        <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>p</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">String: </span><span class=\"token punctuation\">{</span>value<span class=\"token punctuation\">}</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>p</span><span class=\"token punctuation\">></span></span>\n      <span class=\"token punctuation\">)</span><span class=\"token punctuation\">}</span><span class=\"token plain-text\">\n    </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span></span><span class=\"token punctuation\">></span></span>\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span></code></pre></div>\n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token keyword\">const</span> <span class=\"token function-variable function\">App</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">return</span> <span class=\"token punctuation\">(</span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n      </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">Display</span></span> <span class=\"token attr-name\">value</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span><span class=\"token number\">42</span><span class=\"token punctuation\">}</span></span> <span class=\"token punctuation\">/></span></span><span class=\"token plain-text\">\n      </span><span class=\"token punctuation\">{</span><span class=\"token comment\">/* output: Number: 42 */</span><span class=\"token punctuation\">}</span><span class=\"token plain-text\">\n\n      </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">Display</span></span> <span class=\"token attr-name\">value</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>Hello, TypeScript!<span class=\"token punctuation\">\"</span></span> <span class=\"token punctuation\">/></span></span><span class=\"token plain-text\">\n      </span><span class=\"token punctuation\">{</span><span class=\"token comment\">/* output: String: Hello, TypeScript! */</span><span class=\"token punctuation\">}</span><span class=\"token plain-text\">\n    </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span></span><span class=\"token punctuation\">></span></span>\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>The <code class=\"language-text\">typeof</code> check does the narrowing, so both branches know what they’re holding.</p>\n<p>Where unions really earn their place on props is string literals. <code class=\"language-text\">variant: 'primary' | 'secondary' | 'danger'</code> gives you autocomplete in the editor and a compile error on a typo, which is roughly a hundred times more useful than <code class=\"language-text\">variant: string</code> and costs nothing.</p>\n<h3>Reducer actions</h3>\n<p>This is where I use unions most, and it’s the shape that made them click for me. A reducer takes one of a fixed set of actions, and the action’s <code class=\"language-text\">type</code> field tells you which:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">type</span> <span class=\"token class-name\">CounterAction</span> <span class=\"token operator\">=</span>\n  <span class=\"token operator\">|</span> <span class=\"token punctuation\">{</span> type<span class=\"token operator\">:</span> <span class=\"token string\">'increment'</span><span class=\"token punctuation\">;</span> payload<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span> <span class=\"token punctuation\">}</span>\n  <span class=\"token operator\">|</span> <span class=\"token punctuation\">{</span> type<span class=\"token operator\">:</span> <span class=\"token string\">'decrement'</span><span class=\"token punctuation\">;</span> payload<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span> <span class=\"token punctuation\">}</span>\n  <span class=\"token operator\">|</span> <span class=\"token punctuation\">{</span> type<span class=\"token operator\">:</span> <span class=\"token string\">'reset'</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">type</span> <span class=\"token class-name\">CounterState</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span> count<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>Note that <code class=\"language-text\">reset</code> has no payload at all. That’s deliberate, and it’s the part a single flat interface with optional fields cannot express. With <code class=\"language-text\">{ type: string; payload?: number }</code> you can dispatch <code class=\"language-text\">{ type: 'reset', payload: 42 }</code> and nobody stops you.</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">const</span> counterReducer <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span>\n  state<span class=\"token operator\">:</span> CounterState<span class=\"token punctuation\">,</span>\n  action<span class=\"token operator\">:</span> CounterAction\n<span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> CounterState <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">switch</span> <span class=\"token punctuation\">(</span>action<span class=\"token punctuation\">.</span>type<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">case</span> <span class=\"token string\">'increment'</span><span class=\"token operator\">:</span>\n      <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> <span class=\"token operator\">...</span>state<span class=\"token punctuation\">,</span> count<span class=\"token operator\">:</span> state<span class=\"token punctuation\">.</span>count <span class=\"token operator\">+</span> action<span class=\"token punctuation\">.</span>payload <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n    <span class=\"token keyword\">case</span> <span class=\"token string\">'decrement'</span><span class=\"token operator\">:</span>\n      <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> <span class=\"token operator\">...</span>state<span class=\"token punctuation\">,</span> count<span class=\"token operator\">:</span> state<span class=\"token punctuation\">.</span>count <span class=\"token operator\">-</span> action<span class=\"token punctuation\">.</span>payload <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n    <span class=\"token keyword\">case</span> <span class=\"token string\">'reset'</span><span class=\"token operator\">:</span>\n      <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> <span class=\"token operator\">...</span>state<span class=\"token punctuation\">,</span> count<span class=\"token operator\">:</span> <span class=\"token number\">0</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>Inside the <code class=\"language-text\">increment</code> case, <code class=\"language-text\">action.payload</code> is a number and the compiler knows it. Inside <code class=\"language-text\">reset</code>, reaching for <code class=\"language-text\">action.payload</code> is an error, because that member has no such field.</p>\n<h3>Discriminated unions</h3>\n<p>What makes the reducer above work is that every member has a property with a literal type, and the literals are all different. TypeScript calls that a discriminant, and a union built this way is a discriminated union. Switch on it and each <code class=\"language-text\">case</code> narrows the whole object rather than only the field you tested.</p>\n<p>The discriminant does not have to be called <code class=\"language-text\">type</code> and does not have to be a string. Booleans work fine, which is handy for a two-state shape:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">type</span> <span class=\"token class-name\">Guest</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span>\n  id<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">;</span>\n  isGuest<span class=\"token operator\">:</span> <span class=\"token boolean\">true</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">type</span> <span class=\"token class-name\">RegisteredUser</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span>\n  id<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">;</span>\n  isGuest<span class=\"token operator\">:</span> <span class=\"token boolean\">false</span><span class=\"token punctuation\">;</span>\n  email<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">type</span> <span class=\"token class-name\">User</span> <span class=\"token operator\">=</span> Guest <span class=\"token operator\">|</span> RegisteredUser<span class=\"token punctuation\">;</span></code></pre></div>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">const</span> <span class=\"token function-variable function\">getContact</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span>user<span class=\"token operator\">:</span> User<span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>user<span class=\"token punctuation\">.</span>isGuest<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">return</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">return</span> user<span class=\"token punctuation\">.</span>email<span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>Note <code class=\"language-text\">isGuest: true</code>, not <code class=\"language-text\">isGuest: boolean</code>. That distinction is the entire mechanism. A <code class=\"language-text\">boolean</code> field is not a discriminant, it’s just a field, and narrowing will not happen.</p>\n<h4>Exhaustiveness checking</h4>\n<p>Here’s the feature that pays for the rest of it. Assign the narrowed value to <code class=\"language-text\">never</code> in the default branch:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">const</span> counterReducer <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span>\n  state<span class=\"token operator\">:</span> CounterState<span class=\"token punctuation\">,</span>\n  action<span class=\"token operator\">:</span> CounterAction\n<span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> CounterState <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">switch</span> <span class=\"token punctuation\">(</span>action<span class=\"token punctuation\">.</span>type<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">case</span> <span class=\"token string\">'increment'</span><span class=\"token operator\">:</span>\n      <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> <span class=\"token operator\">...</span>state<span class=\"token punctuation\">,</span> count<span class=\"token operator\">:</span> state<span class=\"token punctuation\">.</span>count <span class=\"token operator\">+</span> action<span class=\"token punctuation\">.</span>payload <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n    <span class=\"token keyword\">case</span> <span class=\"token string\">'decrement'</span><span class=\"token operator\">:</span>\n      <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> <span class=\"token operator\">...</span>state<span class=\"token punctuation\">,</span> count<span class=\"token operator\">:</span> state<span class=\"token punctuation\">.</span>count <span class=\"token operator\">-</span> action<span class=\"token punctuation\">.</span>payload <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n    <span class=\"token keyword\">case</span> <span class=\"token string\">'reset'</span><span class=\"token operator\">:</span>\n      <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> <span class=\"token operator\">...</span>state<span class=\"token punctuation\">,</span> count<span class=\"token operator\">:</span> <span class=\"token number\">0</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n    <span class=\"token keyword\">default</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n      <span class=\"token keyword\">const</span> unhandled<span class=\"token operator\">:</span> <span class=\"token builtin\">never</span> <span class=\"token operator\">=</span> action<span class=\"token punctuation\">;</span>\n      <span class=\"token keyword\">throw</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">Error</span><span class=\"token punctuation\">(</span><span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">Unhandled action: </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>unhandled<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n    <span class=\"token punctuation\">}</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>If every case is covered, <code class=\"language-text\">action</code> in the default branch has type <code class=\"language-text\">never</code> and the assignment compiles. The moment somebody adds <code class=\"language-text\">{ type: 'set'; payload: number }</code> to <code class=\"language-text\">CounterAction</code> and forgets the reducer, that line stops compiling and names the type it can’t assign. I’ve caught more bugs with this five-line pattern than with any amount of unit testing around reducers.</p>\n<h3>Combining unions with intersections</h3>\n<p>Unions and intersections are often introduced as a pair, which is a bit misleading because they do opposite things. A union is “one of these”. An intersection, written with <code class=\"language-text\">&amp;</code>, is “all of these at once”.</p>\n<p>They compose usefully. If several members share fields, factor the shared part out and intersect it with the union of the variable parts:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">type</span> <span class=\"token class-name\">WithId</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span> id<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">type</span> <span class=\"token class-name\">Guest</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span> kind<span class=\"token operator\">:</span> <span class=\"token string\">'guest'</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">type</span> <span class=\"token class-name\">Registered</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span> kind<span class=\"token operator\">:</span> <span class=\"token string\">'registered'</span><span class=\"token punctuation\">;</span> email<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">type</span> <span class=\"token class-name\">User</span> <span class=\"token operator\">=</span> WithId <span class=\"token operator\">&amp;</span> <span class=\"token punctuation\">(</span>Guest <span class=\"token operator\">|</span> Registered<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p><code class=\"language-text\">User</code> is equivalent to <code class=\"language-text\">(WithId &amp; Guest) | (WithId &amp; Registered)</code>, so <code class=\"language-text\">id</code> is available everywhere and <code class=\"language-text\">email</code> only appears once you’ve narrowed on <code class=\"language-text\">kind</code>. The intersection distributes over the union. For two members this is more ceremony than it’s worth, but at five or six shared fields it stops the shape from drifting apart.</p>\n<h4>Distribution, and why <code class=\"language-text\">Exclude</code> works</h4>\n<p>Most of the utility types you already use are built on the fact that conditional types distribute over unions. <code class=\"language-text\">Exclude&lt;'a' | 'b' | 'c', 'a'></code> gives <code class=\"language-text\">'b' | 'c'</code> because the condition is applied to each member separately and the results are unioned back together.</p>\n<p>That’s worth knowing mostly so you’re not surprised by it. It’s how you take an existing prop union and derive a narrower one, rather than writing the list out twice and letting the two copies drift.</p>\n<h3>Pitfalls</h3>\n<h4>A union of object types without a discriminant</h4>\n<p>This is the mistake I made for a long time:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">type</span> <span class=\"token class-name\">Success</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span> data<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">type</span> <span class=\"token class-name\">Failure</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span> error<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">type</span> <span class=\"token class-name\">Result</span> <span class=\"token operator\">=</span> Success <span class=\"token operator\">|</span> Failure<span class=\"token punctuation\">;</span></code></pre></div>\n<p>It reads perfectly well and it is annoying to use. You cannot write <code class=\"language-text\">result.data</code>, because <code class=\"language-text\">Failure</code> has no <code class=\"language-text\">data</code>. There is nothing to switch on. You end up with <code class=\"language-text\">'data' in result</code> checks scattered through the code, which works but reads like an apology.</p>\n<p>Add a discriminant and the problem disappears:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">type</span> <span class=\"token class-name\">Result</span> <span class=\"token operator\">=</span>\n  <span class=\"token operator\">|</span> <span class=\"token punctuation\">{</span> status<span class=\"token operator\">:</span> <span class=\"token string\">'success'</span><span class=\"token punctuation\">;</span> data<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span> <span class=\"token punctuation\">}</span>\n  <span class=\"token operator\">|</span> <span class=\"token punctuation\">{</span> status<span class=\"token operator\">:</span> <span class=\"token string\">'error'</span><span class=\"token punctuation\">;</span> error<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>If you’re modelling API responses this way, it pairs nicely with the shape normalisation I wrote about in <a href=\"/blog/normalizr/\">Using Normalizr</a>: decide the shape once, at the boundary, and the rest of the app stops guessing.</p>\n<h4>Widening swallows your literals</h4>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">type</span> <span class=\"token class-name\">Size</span> <span class=\"token operator\">=</span> <span class=\"token string\">'sm'</span> <span class=\"token operator\">|</span> <span class=\"token string\">'md'</span> <span class=\"token operator\">|</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>That looks like “one of these two, or any other string”. It is not. <code class=\"language-text\">'sm'</code> and <code class=\"language-text\">'md'</code> are both assignable to <code class=\"language-text\">string</code>, so the whole thing collapses to <code class=\"language-text\">string</code> and your autocomplete quietly disappears. There’s no error, which is what makes it nasty.</p>\n<p>If you genuinely want the suggestions plus an escape hatch, the workaround is to stop the collapse:</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">type</span> <span class=\"token class-name\">Size</span> <span class=\"token operator\">=</span> <span class=\"token string\">'sm'</span> <span class=\"token operator\">|</span> <span class=\"token string\">'md'</span> <span class=\"token operator\">|</span> <span class=\"token punctuation\">(</span><span class=\"token builtin\">string</span> <span class=\"token operator\">&amp;</span> <span class=\"token punctuation\">{</span><span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>It’s ugly. It works, the editor keeps suggesting <code class=\"language-text\">sm</code> and <code class=\"language-text\">md</code>, and I use it perhaps twice a year.</p>\n<h4>Too many members</h4>\n<p>A union with fifteen members is a signal, not an achievement. Somewhere in there is a second concept trying to get out. Split it, or find the field the members actually vary on and make that the discriminant.</p>\n<p>The same thing applies to a component whose props are a union of four completely different shapes. That’s usually four components sharing a name because nobody wanted to pick three more. Some of this is just <a href=\"/blog/idiomatic-javascript/\">idiomatic programming</a> applied to types: the clearest version is the one the next person can read without a map.</p>\n<h3>Where I’d start</h3>\n<p>If you take unions no further than string literal props and one discriminated reducer action type with a <code class=\"language-text\">never</code> check in the default branch, you have most of the value. Everything above that is refinement.</p>\n<p>The one thing I’d add on top is the habit of asking, whenever a field is optional, whether it’s actually optional or whether there are two shapes hiding behind one interface. It usually is two shapes.</p>\n<p>‘Till next time!</p>","rawMarkdownBody":"\n## The pipe character does a lot of work\n\n<p class=\"lead\">A union type is the smallest possible piece of TypeScript syntax and one of the easiest to get subtly wrong. You write a pipe between two types and the compiler starts asking questions you didn't expect.</p>\n\nI want to go through how I actually use unions in React work: on props, on reducer actions, and in the places where the compiler stops helping and you have to give it a hand. There is also a small pile of things I got wrong for longer than I'd like to admit, so those get their own section at the end.\n\n### What a union actually is\n\nA union says a value is one of several types, but not which one. You write it with a pipe:\n\n```typescript\ntype MyUnion = number | string;\n```\n\n`MyUnion` holds either a number or a string. The important half of that sentence is the part people skip: until you prove which one it is, TypeScript will only let you use the members that are common to both. `.toFixed()` is off limits, `.toUpperCase()` is off limits, and `.toString()` is fine because both have it.\n\nProving which one it is called narrowing, and it is most of the work.\n\n#### Function parameters\n\nThe classic case is a parameter that accepts one thing or a list of them:\n\n```typescript\nconst greet = (names: string | string[]) => {\n  if (Array.isArray(names)) {\n    names.forEach(name => console.log(`Hello, ${name}!`));\n  } else {\n    console.log(`Hello, ${names}!`);\n  }\n};\n```\n\n```typescript\ngreet('Alice');\n// output: Hello, Alice!\n\ngreet(['Bob', 'Charlie']);\n// output: Hello, Bob! Hello, Charlie!\n```\n\nInside the `if`, `names` is `string[]`. Inside the `else`, it's `string`. `Array.isArray` is one of the checks TypeScript understands natively, along with `typeof`, `instanceof`, `in`, and a plain truthiness test.\n\n#### Conditional rendering in React\n\nSame idea, applied to props. A component that renders differently depending on what it was handed:\n\n```tsx\ntype DisplayProps = { value: number | string };\n\nconst Display: React.FC<DisplayProps> = ({ value }) => {\n  return (\n    <>\n      {typeof value === 'number' ? (\n        <p>Number: {value}</p>\n      ) : (\n        <p>String: {value}</p>\n      )}\n    </>\n  );\n};\n```\n\n```tsx\nconst App = () => {\n  return (\n    <>\n      <Display value={42} />\n      {/* output: Number: 42 */}\n\n      <Display value=\"Hello, TypeScript!\" />\n      {/* output: String: Hello, TypeScript! */}\n    </>\n  );\n};\n```\n\nThe `typeof` check does the narrowing, so both branches know what they're holding.\n\nWhere unions really earn their place on props is string literals. `variant: 'primary' | 'secondary' | 'danger'` gives you autocomplete in the editor and a compile error on a typo, which is roughly a hundred times more useful than `variant: string` and costs nothing.\n\n### Reducer actions\n\nThis is where I use unions most, and it's the shape that made them click for me. A reducer takes one of a fixed set of actions, and the action's `type` field tells you which:\n\n```typescript\ntype CounterAction =\n  | { type: 'increment'; payload: number }\n  | { type: 'decrement'; payload: number }\n  | { type: 'reset' };\n\ntype CounterState = { count: number };\n```\n\nNote that `reset` has no payload at all. That's deliberate, and it's the part a single flat interface with optional fields cannot express. With `{ type: string; payload?: number }` you can dispatch `{ type: 'reset', payload: 42 }` and nobody stops you.\n\n```typescript\nconst counterReducer = (\n  state: CounterState,\n  action: CounterAction\n): CounterState => {\n  switch (action.type) {\n    case 'increment':\n      return { ...state, count: state.count + action.payload };\n    case 'decrement':\n      return { ...state, count: state.count - action.payload };\n    case 'reset':\n      return { ...state, count: 0 };\n  }\n};\n```\n\nInside the `increment` case, `action.payload` is a number and the compiler knows it. Inside `reset`, reaching for `action.payload` is an error, because that member has no such field.\n\n### Discriminated unions\n\nWhat makes the reducer above work is that every member has a property with a literal type, and the literals are all different. TypeScript calls that a discriminant, and a union built this way is a discriminated union. Switch on it and each `case` narrows the whole object rather than only the field you tested.\n\nThe discriminant does not have to be called `type` and does not have to be a string. Booleans work fine, which is handy for a two-state shape:\n\n```typescript\ntype Guest = {\n  id: string;\n  isGuest: true;\n};\n\ntype RegisteredUser = {\n  id: string;\n  isGuest: false;\n  email: string;\n};\n\ntype User = Guest | RegisteredUser;\n```\n\n```typescript\nconst getContact = (user: User) => {\n  if (user.isGuest) {\n    return null;\n  }\n\n  return user.email;\n};\n```\n\nNote `isGuest: true`, not `isGuest: boolean`. That distinction is the entire mechanism. A `boolean` field is not a discriminant, it's just a field, and narrowing will not happen.\n\n#### Exhaustiveness checking\n\nHere's the feature that pays for the rest of it. Assign the narrowed value to `never` in the default branch:\n\n```typescript\nconst counterReducer = (\n  state: CounterState,\n  action: CounterAction\n): CounterState => {\n  switch (action.type) {\n    case 'increment':\n      return { ...state, count: state.count + action.payload };\n    case 'decrement':\n      return { ...state, count: state.count - action.payload };\n    case 'reset':\n      return { ...state, count: 0 };\n    default: {\n      const unhandled: never = action;\n      throw new Error(`Unhandled action: ${unhandled}`);\n    }\n  }\n};\n```\n\nIf every case is covered, `action` in the default branch has type `never` and the assignment compiles. The moment somebody adds `{ type: 'set'; payload: number }` to `CounterAction` and forgets the reducer, that line stops compiling and names the type it can't assign. I've caught more bugs with this five-line pattern than with any amount of unit testing around reducers.\n\n### Combining unions with intersections\n\nUnions and intersections are often introduced as a pair, which is a bit misleading because they do opposite things. A union is \"one of these\". An intersection, written with `&`, is \"all of these at once\".\n\nThey compose usefully. If several members share fields, factor the shared part out and intersect it with the union of the variable parts:\n\n```typescript\ntype WithId = { id: string };\n\ntype Guest = { kind: 'guest' };\ntype Registered = { kind: 'registered'; email: string };\n\ntype User = WithId & (Guest | Registered);\n```\n\n`User` is equivalent to `(WithId & Guest) | (WithId & Registered)`, so `id` is available everywhere and `email` only appears once you've narrowed on `kind`. The intersection distributes over the union. For two members this is more ceremony than it's worth, but at five or six shared fields it stops the shape from drifting apart.\n\n#### Distribution, and why `Exclude` works\n\nMost of the utility types you already use are built on the fact that conditional types distribute over unions. `Exclude<'a' | 'b' | 'c', 'a'>` gives `'b' | 'c'` because the condition is applied to each member separately and the results are unioned back together.\n\nThat's worth knowing mostly so you're not surprised by it. It's how you take an existing prop union and derive a narrower one, rather than writing the list out twice and letting the two copies drift.\n\n### Pitfalls\n\n#### A union of object types without a discriminant\n\nThis is the mistake I made for a long time:\n\n```typescript\ntype Success = { data: string };\ntype Failure = { error: string };\n\ntype Result = Success | Failure;\n```\n\nIt reads perfectly well and it is annoying to use. You cannot write `result.data`, because `Failure` has no `data`. There is nothing to switch on. You end up with `'data' in result` checks scattered through the code, which works but reads like an apology.\n\nAdd a discriminant and the problem disappears:\n\n```typescript\ntype Result =\n  | { status: 'success'; data: string }\n  | { status: 'error'; error: string };\n```\n\nIf you're modelling API responses this way, it pairs nicely with the shape normalisation I wrote about in [Using Normalizr](/blog/normalizr/): decide the shape once, at the boundary, and the rest of the app stops guessing.\n\n#### Widening swallows your literals\n\n```typescript\ntype Size = 'sm' | 'md' | string;\n```\n\nThat looks like \"one of these two, or any other string\". It is not. `'sm'` and `'md'` are both assignable to `string`, so the whole thing collapses to `string` and your autocomplete quietly disappears. There's no error, which is what makes it nasty.\n\nIf you genuinely want the suggestions plus an escape hatch, the workaround is to stop the collapse:\n\n```typescript\ntype Size = 'sm' | 'md' | (string & {});\n```\n\nIt's ugly. It works, the editor keeps suggesting `sm` and `md`, and I use it perhaps twice a year.\n\n#### Too many members\n\nA union with fifteen members is a signal, not an achievement. Somewhere in there is a second concept trying to get out. Split it, or find the field the members actually vary on and make that the discriminant.\n\nThe same thing applies to a component whose props are a union of four completely different shapes. That's usually four components sharing a name because nobody wanted to pick three more. Some of this is just [idiomatic programming](/blog/idiomatic-javascript/) applied to types: the clearest version is the one the next person can read without a map.\n\n### Where I'd start\n\nIf you take unions no further than string literal props and one discriminated reducer action type with a `never` check in the default branch, you have most of the value. Everything above that is refinement.\n\nThe one thing I'd add on top is the habit of asking, whenever a field is optional, whether it's actually optional or whether there are two shapes hiding behind one interface. It usually is two shapes.\n\n'Till next time!\n","frontmatter":{"title":"Understanding TypeScript Unions","date":"18. July, 2023","description":"A practical guide to TypeScript union types in React, covering narrowing, discriminated unions, intersections and the pitfalls that quietly cost you time.","category":"Develop","cover":{"childImageSharp":{"gatsbyImageData":{"layout":"fixed","backgroundColor":"#282828","images":{"fallback":{"src":"/static/5d9b85cd4def90c23c4f8a4020e62711/1619f/union.png","srcSet":"/static/5d9b85cd4def90c23c4f8a4020e62711/1619f/union.png 960w","sizes":"960px"},"sources":[{"srcSet":"/static/5d9b85cd4def90c23c4f8a4020e62711/0a27d/union.webp 960w","type":"image/webp","sizes":"960px"}]},"width":960,"height":653}}}},"fields":{"slug":"/2023-07-18_typescript-union/"}}},"pageContext":{"slug":"/2023-07-18_typescript-union/","previous":{"fields":{"slug":"/2023-06-13_how-ai-changes-coding/"},"frontmatter":{"title":"AI and Development"}},"next":{"fields":{"slug":"/2023-08-20_react-use-context/"},"frontmatter":{"title":"Exploring React's useContext Hook"}}}},
    "staticQueryHashes": ["1711471402","674253978"]}