<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[kimmo.blog]]></title><description><![CDATA[Blog about web-focused software development with a hint of business, design, and tiny perfect details.]]></description><link>https://kimmo.blog</link><image><url>https://kimmo.blog/icon-square.png</url><title>kimmo.blog</title><link>https://kimmo.blog</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 19 Apr 2023 19:16:17 GMT</lastBuildDate><atom:link href="https://kimmo.blog/rss.xml" rel="self" type="application/rss+xml"/><item><title><![CDATA[Writing, or coding]]></title><description><![CDATA[Do I want to write content, or do I want to code a blog?
]]></description><link>https://kimmo.blog/posts/1-writing-or-coding/</link><guid isPermaLink="false">https://kimmo.blog/posts/1-writing-or-coding/</guid><category><![CDATA[meta]]></category><category><![CDATA[blog]]></category><category><![CDATA[content creation]]></category><pubDate>Sat, 07 Aug 2021 00:00:00 GMT</pubDate><enclosure url="https://kimmo.blog/content-assets/1/cover.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<html><head></head><body><div data-reactroot=""><p class="max-w-xl font-sans mb-paragraph ">I was setting up this blog at the beginning of 2021 and excitedly told a few developer friends about the plan. The discussion naturally revolved around blogging platforms and tech choices, until one of them asked:</p><div class="ChatDiscussion py-10 mx-auto md:relative md:-left-6 max-w-xs flex flex-col"><p class="ChatMessage from-them">Do you actually want to write content, or do you want to code a blog?</p></div><p class="max-w-xl font-sans mb-paragraph ">It's a great question. I wasn't sure about it, so I set up a rule for myself: write three complete posts before publishing the blog. It was a good decision, because it took almost a year to reach three blog posts. <em class="italic">So, what took so long?</em></p><p class="max-w-xl font-sans mb-paragraph ">Turns out I wanted to code a bit more — at least in the beginning. I spent way too much time building an over-engineered React site, considering the goal was to write content. <em class="italic">But so what?</em> A hobby project should be inspiring and free of rules, not a KPI-driven piece of work. Unless that's your thing of course. For me, implementing the site from scratch felt like a necessary fuel for my writing inspiration. I didn't want to code everything just for the sake of it though. I'll try to explain.</p><p class="max-w-xl font-sans mb-paragraph ">For years, I've gone back and forth with different blogging platforms. <a href="https://medium.com/@kimmobrunfeldt/introduction-to-sauce-labs-3ad082fc7dd5" class="cursor-pointer underline hover:underline ">Medium</a> for a few testing-related posts, <a href="http://scouringmacbook.blogspot.com/2012/10/setting-everything.html" class="cursor-pointer underline hover:underline ">Blogger</a> for macOS configuration posts, WordPress for a business website, <a href="https://github.com/kimmobrunfeldt/howto-everything" class="cursor-pointer underline hover:underline ">Github repo</a> with <code>.md</code> files, and many open-source solutions like Jekyll, Ghost, Hugo, and Hexo.</p><p class="max-w-xl font-sans mb-paragraph "><em class="italic">Let's talk about the hosted platforms first.</em></p><div class="my-12"><video width="100%" style="border-radius:3px" preload="auto" autoplay="" muted="" controls=""><source src="https://kimmo.blog/content-assets/1/1-notifications.mp4" type="video/mp4"></video><p class="italic text-sm mt-4 text-center">What nobody wants</p></div><p class="max-w-xl font-sans mb-paragraph ">The goal of this blog is to share my thoughts and ideally help other people. To achieve that, there's no need to ask for users' locations, ask if they want to receive notifications via service workers, or anything like that. There's just the content in a minimal digital frame.</p><p class="max-w-xl font-sans mb-paragraph ">Talking about content — it should be accessible, searchable, open, and permanent. Not many hosted services share these ideologies. That's completely understandable because they have a business model to sustain.</p><p class="max-w-xl font-sans mb-paragraph "><em class="italic">What about open source solutions?</em></p><p class="max-w-xl font-sans mb-paragraph ">Self-hosted open source blogging tools are very polished and felt tempting, but I had a feeling that hacking existing solutions wasn't interesting enough.</p><p class="max-w-xl font-sans mb-paragraph ">The content I want to share is tightly related to code and my vision is to make the posts <a href="https://www.joshwcomeau.com/animation/a-friendly-introduction-to-spring-physics/" class="cursor-pointer underline hover:underline ">interactive</a>, <a href="http://www.r2d3.us/visual-intro-to-machine-learning-part-1/" class="cursor-pointer underline hover:underline ">visual</a>, and <a href="https://jgthms.com/web-design-in-4-minutes/" class="cursor-pointer underline hover:underline ">understandable</a>. I'm thinking:</p><blockquote class="text-gray-6 dark:text-gray-5 italic py-2 px-7 my-6 text-center text-lg md:relative md:-left-6"><p class="max-w-xl font-sans mb-paragraph ">If I start a blog, the platform shouldn't come in the way of expressing ideas.</p></blockquote><p class="max-w-xl font-sans mb-paragraph ">In addition to the subjective arguments and feelings, I gathered a list of other requirements:</p><ul class="pl-6 list-disc space-y-1"><li class="md:pl-2">Custom domain</li><li class="md:pl-2">Permanent URLs for posts (10+ years)</li><li class="md:pl-2">Technically flexible for possible crazy ideas in the future: <em class="italic">code highlighting</em>, <em class="italic">SVG animations</em>, <em class="italic">interactive charts</em>, and <em class="italic">any custom components</em> that help understanding concepts</li><li class="md:pl-2">Content should be readable without JavaScript</li></ul><p class="max-w-xl font-sans mb-paragraph ">I wanted to write, but also see what kind of page I can put together. So DIY it was. It felt like the most interesting and fun approach. Since the posts would have dynamic components and visualisations, I chose React and <a href="https://mdxjs.com/" class="cursor-pointer underline hover:underline ">MDX</a>. They were familiar tools.</p><p class="max-w-xl font-sans mb-paragraph ">If you've agreed with my decisions until this point, that's nice! However, reinventing the wheel is only about to start.</p><p class="max-w-xl font-sans mb-paragraph ">Lately, I've been obsessed with web performance. It's starting to get normal for a website to download 15 MB of uncompressed files to display simple information. I wanted to make this site <a href="https://kimmo.blog/perf/" class="cursor-pointer underline hover:underline ">as minimal in size as possible</a> <em class="italic">(sidenote: perf score suffers from tiny CI instances)</em> while considering the vision of interactive and dynamic content.</p><div class="ChatDiscussion py-10 mx-auto md:relative md:-left-6 max-w-xs flex flex-col"><p class="ChatMessage from-them">Alright, I kind of get what you're saying. Will you be using Gatsby or React Static then?</p><p class="ChatMessage from-me">Well...</p></div><p class="max-w-xl font-sans mb-paragraph ">After a lot of initial hesitation between do-it-yourself or <code>npm i gatsby</code>, I started implementing a Gatsby-like static site generator. It sounded like a huge time sink but I wanted to know how minimal it can get. The benefits of a custom static site generator compared to Gatsby would be most probably microscopical, but why not? This project was supposed to be an inspiring and fun project after all!</p><p class="max-w-xl font-sans mb-paragraph ">While crossing the implementation finish line, I realised that the content isn't just the last 10% — it's the last 95%. The realisation killed my writing motivation for a while. There weren't any CSS transitions to tweak anymore, just the half-written blog posts waiting. But when days go by, more and more thoughts are bubbling up. Now I finally have the domain, tech stack, repository, and existing <code>.mdx</code> files craving to get some juicy content.</p></div></body></html>]]></content:encoded><content:media url="https://kimmo.blog/content-assets/1/cover.png" type="image/jpeg" medium="image" xmlns:media="http://search.yahoo.com/mrss/"/></item><item><title><![CDATA[DIY Gatsby: Static site rendering]]></title><description><![CDATA[Part I of a series where we'll look into how this blog was made. This part covers how static site rendering works in general.
]]></description><link>https://kimmo.blog/posts/2-diy-gatsby-part-i/</link><guid isPermaLink="false">https://kimmo.blog/posts/2-diy-gatsby-part-i/</guid><category><![CDATA[static site generator]]></category><category><![CDATA[react]]></category><category><![CDATA[server-side rendering]]></category><category><![CDATA[gatsby]]></category><pubDate>Tue, 24 Aug 2021 00:00:00 GMT</pubDate><enclosure url="https://kimmo.blog/content-assets/2/cover.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<html><head></head><body><div data-reactroot=""><p class="max-w-xl font-sans mb-paragraph "><em class="italic">This is Part<!-- -->&nbsp;<!-- -->I of a series where we'll look into how kimmo.blog works. Part<!-- -->&nbsp;<!-- -->I covers<!-- --> <abbr title="Server-side Rendering">SSR</abbr> and static site basics. If you are familiar with the topic, feel free to jump into <a href="https://kimmo.blog/posts/3-diy-gatsby-part-ii/" class="cursor-pointer underline hover:underline ">Part&nbsp;II: Module bundling</a>.</em></p><p class="max-w-xl font-sans mb-paragraph "><em class="italic">Motivation has been covered in <a href="https://kimmo.blog/posts/1-writing-or-coding/" class="cursor-pointer underline hover:underline ">Writing, or coding</a>.</em></p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="disclaimers">Disclaimers</h2><p class="max-w-xl font-sans mb-paragraph "><a href="https://www.gatsbyjs.com/" class="cursor-pointer underline hover:underline ">Gatsby</a> is a production-ready tool that has everything you need: customisable data sources accessed via GraphQL, optimised build pipeline, functionality via plugins, and a huge community.</p><p class="max-w-xl font-sans mb-paragraph ">The build system that we'll go through on the other hand is experimental and doesn't even have frontend routing. Building a better Gatsby is not the point. Instead, we want to understand how it works and possibly make the site's bundle footprint even smaller.</p><p class="max-w-xl font-sans mb-paragraph ">That out of the way, let's jump into building the thing.</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="static-site-rendering">Static site rendering</h2><p class="max-w-xl font-sans mb-paragraph ">The high-level idea is pretty straightforward: convert a bunch of source files and content to a static site in <code>output/</code> directory.</p><div class="select-none  mt-12 mb-16"><ul class="flex flex-row justify-end mb-2"><li><button type="button" class="cursor-not-allowed text-gray-3 dark:text-gray-7 block top-[1px]" title="Previous" aria-label="Previous animation state" disabled=""><span style="font-size:1.3em"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="box-content p-2" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M8.854 4.854l.353-.354l-.707-.707l-.354.353l.708.708zM5.5 7.5l-.354-.354l-.353.354l.353.354L5.5 7.5zm2.646 3.354l.354.353l.707-.707l-.353-.354l-.708.708zm0-6.708l-3 3l.708.708l3-3l-.708-.708zm-3 3.708l3 3l.708-.708l-3-3l-.708.708zM7.5 14A6.5 6.5 0 0 1 1 7.5H0A7.5 7.5 0 0 0 7.5 15v-1zM14 7.5A6.5 6.5 0 0 1 7.5 14v1A7.5 7.5 0 0 0 15 7.5h-1zM7.5 1A6.5 6.5 0 0 1 14 7.5h1A7.5 7.5 0 0 0 7.5 0v1zm0-1A7.5 7.5 0 0 0 0 7.5h1A6.5 6.5 0 0 1 7.5 1V0z" fill="currentColor"></path></g></svg></span></button></li><li><button type="button" class="text-rust-6 hover:text-rust-5 active:text-rust-7 dark:text-rust-6 dark:active:text-rust-5 block top-[1px]" title="Back to start" aria-label="Back to start"><span style="font-size:1.3em"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="box-content p-2" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M14 8.495v-.5h-1v.5h1zM7.5 2.999H8v-1h-.5v1zm1-.5l.353.353l.354-.353l-.354-.354l-.353.354zM13 8.495a5.499 5.499 0 0 1-5.5 5.496v1c3.589 0 6.5-2.909 6.5-6.496h-1zM7.5 13.99A5.499 5.499 0 0 1 2 8.495H1a6.499 6.499 0 0 0 6.5 6.496v-1zM2 8.495a5.499 5.499 0 0 1 5.5-5.496v-1A6.499 6.499 0 0 0 1 8.495h1zM6.147.854l2 1.998l.706-.707l-2-1.999l-.706.708zm2 1.291l-2 1.999l.706.707l2-1.999l-.706-.707z" fill="currentColor"></path></g></svg><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="box-content p-2 hidden" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M6.146 10.146l-.353.354l.707.707l.354-.353l-.708-.708zM9.5 7.5l.354.354l.353-.354l-.353-.354L9.5 7.5zM6.854 4.146L6.5 3.793l-.707.707l.353.354l.708-.708zm0 6.708l3-3l-.708-.708l-3 3l.708.708zm3-3.708l-3-3l-.708.708l3 3l.708-.708zM7.5 1A6.5 6.5 0 0 1 14 7.5h1A7.5 7.5 0 0 0 7.5 0v1zM1 7.5A6.5 6.5 0 0 1 7.5 1V0A7.5 7.5 0 0 0 0 7.5h1zM7.5 14A6.5 6.5 0 0 1 1 7.5H0A7.5 7.5 0 0 0 7.5 15v-1zm0 1A7.5 7.5 0 0 0 15 7.5h-1A6.5 6.5 0 0 1 7.5 14v1z" fill="currentColor"></path></g></svg></span></button></li></ul><div class=" mx-auto"><noscript>Animation requires JavaScript</noscript><img alt="Build pipeline overview presentation" src="https://kimmo.blog/content-assets/2/build-pipeline-overview.svg"></div></div><p class="max-w-xl font-sans mb-paragraph ">We "<a href="https://www.knowledgeowl.com/home/dont-say-simply-jim-fisher" class="cursor-pointer underline hover:underline ">simply</a>" need to implement the <em class="italic">Build pipeline</em> box in the middle.</p><p class="max-w-xl font-sans mb-paragraph ">One way to start figuring out a system is from the user's perspective. Let's imagine there's a file server that serves everything under <code>output/</code> directory.</p><div class=" my-12"><a href="https://kimmo.blog/content-assets/2/get-request.svg" rel="noreferrer" target="_blank"><img class="mx-auto w-full" alt="Figure of GET request" src="https://kimmo.blog/content-assets/2/get-request.svg" style="max-width:550px"></a></div><p class="max-w-xl font-sans mb-paragraph ">When a user loads <code>/posts/</code> path in their browser, the server responds with the contents of <code>output/posts/index.html</code> file. The contents have been created with Node.js using React:</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token keyword">import</span><span class="token plain"> </span><span class="token maybe-class-name">ReactDOMServer</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"react-dom/server"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> </span><span class="token maybe-class-name">AllPostsPage</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"./pages/AllPosts"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token maybe-class-name">ReactDOMServer</span><span class="token punctuation">.</span><span class="token function">renderToString</span><span class="token punctuation">(</span><span class="token tag punctuation">&lt;</span><span class="token tag class-name">AllPostsPage</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">This type of React usage is often called server-side rendering – regardless of where the rendering technically took place. <code>renderToString()</code> might've been called on your local machine, Netlify's build servers, or per request on a Node.js server.</p><blockquote class="text-gray-6 dark:text-gray-5 italic py-2 px-7 my-6 text-center text-lg md:relative md:-left-6"><p class="max-w-xl font-sans mb-paragraph ">Why do we even need this step? React apps work just fine even if the backend sends an empty <code>&lt;div id="react-root"&gt;&lt;/div&gt;</code> container.</p></blockquote><p class="max-w-xl font-sans mb-paragraph ">That's true. Server-side rendering is not mandatory, but it has some benefits. It'll make the content load faster, more<!-- --> <abbr title="Search Engine Optimization">SEO</abbr> friendly, and accessible even when JS has been disabled.</p><p class="max-w-xl font-sans mb-paragraph ">That covers the read-only content, but for richer interaction, we need to generate a JS bundle that kicks off the dynamism browser-side. The traditional way would be to render HTML directly from e.g. Markdown, and JS would refer to the existing<!-- --> <abbr title="Document Object Model">DOM</abbr> elements via query selectors:</p><pre class="py-0"><code class="language-javascript"><div class="token-line"><span class="token keyword">const</span><span class="token plain"> elements </span><span class="token operator">=</span><span class="token plain"> </span><span class="token dom variable">document</span><span class="token punctuation">.</span><span class="token method function property-access">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'.chart'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token function">initCharts</span><span class="token punctuation">(</span><span class="token plain">elements</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">There are many benefits to the traditional model, no doubt about it. However, it's missing some of the ergonomics we get from React — for example, component separation and state handling. Anyways, arguing about whether using React in this context makes sense is not the topic of this post.</p><div class=" my-12"><a href="https://kimmo.blog/content-assets/2/get-request-hydrate.svg" rel="noreferrer" target="_blank"><img class="mx-auto w-full" alt="Figure of another GET request" src="https://kimmo.blog/content-assets/2/get-request-hydrate.svg" style="max-width:550px"></a></div><p class="max-w-xl font-sans mb-paragraph ">Building on top of the previous example, let's add a JS bundle called <code>hydrate.js</code> to the page. It uses <a href="https://reactjs.org/docs/react-dom.html#hydrate" class="cursor-pointer underline hover:underline ">ReactDOM.hydrate()</a> to continue where the server-side rendered React left off:</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token keyword">import</span><span class="token plain"> </span><span class="token maybe-class-name">React</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"react"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token maybe-class-name">ReactDOM</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"react-dom"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> </span><span class="token maybe-class-name">AllPostsPage</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"./pages/AllPosts"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token dom variable">window</span><span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"load"</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token operator">=&gt;</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token maybe-class-name">ReactDOM</span><span class="token punctuation">.</span><span class="token function">hydrate</span><span class="token punctuation">(</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag class-name">AllPostsPage</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token dom variable">document</span><span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'react-root'</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">The<!-- --> <abbr title="Document Object Model">DOM</abbr> stays untouched <em class="italic">(<code>&lt;h1&gt;All posts&lt;/h1&gt;</code>, etc)</em>, but React starts event listeners to allow user interaction on the page.</p><p class="max-w-xl font-sans mb-paragraph "><strong class="font-bold">That's how server-side rendering works.</strong> Backend returns a frozen snapshot of a React component and frontend brings it back to life.</p><p class="max-w-xl font-sans mb-paragraph ">There are benefits, but definitely some downsides too. First of all, the rendering flow is very complex compared to a single page app. Another annoyance is that most component code paths will be run on Node.js too. <a href="https://github.com/Splidejs/splide/issues/278" class="cursor-pointer underline hover:underline ">Many</a> <a href="https://github.com/JedWatson/react-select/issues/608" class="cursor-pointer underline hover:underline ">frontend</a> <a href="https://github.com/codex-team/editor.js/issues/1036" class="cursor-pointer underline hover:underline ">libraries</a> throw <code>ReferenceError: window is not defined</code> because they weren't designed to be run in a <code>window</code>less environment. That means you'll have one more scenario to test.</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="spa-vs-static-site"><abbr title="Single Page App">SPA</abbr> vs static site</h2><p class="max-w-xl font-sans mb-paragraph ">In single page apps, the server is usually configured to send the same <code>index.html</code> with an empty <code>&lt;div id="react-root" /&gt;</code> container for any request path. JavaScript would then read <code>window.location</code> and decide which page content to render based on the frontend routing. But we don't want that, because it will inevitably be at least a little bit slower than the server-side rendered content at page load.</p><p class="max-w-xl font-sans mb-paragraph ">In static sites, each page needs to be able to independently bootstrap the JavaScript UI. In other words, static sites can have multiple entrypoints into the application.</p><p class="max-w-xl font-sans mb-paragraph ">I wouldn't say there's a clear winner, just different approaches with varying pros and cons. Also, I'm going to conveniently just skip the fact that the custom build system doesn't support frontend routing. That will make navigation after the initial page load slightly slower than in SPAs. It was a shortcut that I took to make the setup less complex. Hats off to Gatsby for actually managing the complexity.</p><p class="max-w-xl font-sans mb-paragraph ">That's static site rendering basics. There's a ton of details that still need to be solved though. In <a href="https://kimmo.blog/posts/3-diy-gatsby-part-ii/" class="cursor-pointer underline hover:underline ">Part<!-- -->&nbsp;<!-- -->II</a>, we'll look into code specifics and module bundling with Rollup.</p></div></body></html>]]></content:encoded><content:media url="https://kimmo.blog/content-assets/2/cover.png" type="image/jpeg" medium="image" xmlns:media="http://search.yahoo.com/mrss/"/></item><item><title><![CDATA[DIY Gatsby: Module bundling]]></title><description><![CDATA[Part II of a series where we'll look into how this blog was made. The second part focuses on Rollup, task runners, and page generation.
]]></description><link>https://kimmo.blog/posts/3-diy-gatsby-part-ii/</link><guid isPermaLink="false">https://kimmo.blog/posts/3-diy-gatsby-part-ii/</guid><category><![CDATA[module bundling]]></category><category><![CDATA[rollup]]></category><category><![CDATA[react]]></category><category><![CDATA[gatsby]]></category><category><![CDATA[npm scripts]]></category><pubDate>Tue, 31 Aug 2021 00:00:00 GMT</pubDate><enclosure url="https://kimmo.blog/content-assets/3/cover.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<html><head></head><body><div data-reactroot=""><p class="max-w-xl font-sans mb-paragraph "><em class="italic">This is Part<!-- -->&nbsp;<!-- -->II of a series where we'll look into how kimmo.blog works. Part<!-- -->&nbsp;<!-- -->II focuses on how <a href="https://rollupjs.org/guide/en/" class="cursor-pointer underline hover:underline ">Rollup</a>, custom glue code, and templates are used to convert source files into pages that work in all browsers.</em></p><p class="max-w-xl font-sans mb-paragraph "><em class="italic">Motivation has been covered in <a href="https://kimmo.blog/posts/1-writing-or-coding/" class="cursor-pointer underline hover:underline ">Writing, or coding</a>.</em></p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="module-bundler">Module bundler</h2><div class=" my-12"><a href="https://kimmo.blog/content-assets/3/dir-structure-simple.svg" rel="noreferrer" target="_blank"><img class="mx-auto w-full" alt="Directory structures" src="https://kimmo.blog/content-assets/3/dir-structure-simple.svg"></a></div><p class="max-w-xl font-sans mb-paragraph ">We need a way to convert <code>.tsx</code> source files into JS bundles. TypeScript compiler does the job in simple cases, but we want a full-blown module bundler. <em class="italic">Why?</em> Because module bundler is able to handle also other tasks required to build a fast production site: tree shaking, bundle splitting, import aliases, <code>process.env</code> replacements, etc.</p><p class="max-w-xl font-sans mb-paragraph ">I looked into <a href="https://webpack.js.org/" class="cursor-pointer underline hover:underline ">Webpack</a>, <a href="https://www.snowpack.dev/" class="cursor-pointer underline hover:underline ">Snowpack</a>, <a href="https://parceljs.org/" class="cursor-pointer underline hover:underline ">Parcel</a>, <a href="https://rollupjs.org/" class="cursor-pointer underline hover:underline ">Rollup</a>, and <a href="https://babeljs.io/" class="cursor-pointer underline hover:underline ">Babel</a>. They all have slightly different abstraction levels and philosophies, but Rollup was my choice for the task. Its configuration format is simple and the core is very minimal. Rollup allows precise low-level control over the bundling process.</p><p class="max-w-xl font-sans mb-paragraph ">More control usually equals more work. Rollup is no exception. Most tasks that Webpack and other bundlers do by default, need a plugin in Rollup. The explicit configuration and minimal core force you to think about details though.</p><blockquote class="text-gray-6 dark:text-gray-5 italic py-2 px-7 my-6 text-center text-lg md:relative md:-left-6"><p class="max-w-xl font-sans mb-paragraph ">If a dependency uses Node.js core library such as <code>crypto</code>, do you want to polyfill it?</p></blockquote><p class="max-w-xl font-sans mb-paragraph ">It sounds tedious, but on the other hand it makes you very aware of what's happening under the hood. The payoff is increased understanding of the build process and most probably a smaller bundle size.</p><p class="max-w-xl font-sans mb-paragraph ">This is how the simplified <a href="https://github.com/kimmobrunfeldt/blog/blob/main/rollup.config.js" class="cursor-pointer underline hover:underline ">rollup.config.js</a> for this blog looks like:</p><pre class="py-0"><code class="language-ts"><div class="token-line"><span class="token keyword">export</span><span class="token plain"> </span><span class="token keyword">default</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token comment">// Use tmp dir as a workaround. Ideally the output dir</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token comment">// would be modified in-place, but that didn't work with</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token comment">// Rollup for some reason.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  input</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token string">"output-tmp-rollup/**/*.tsx"</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">"output-tmp-rollup/**/*.ts"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  output</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    dir</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">"output"</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    format</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">"esm"</span><span class="token punctuation">,</span><span class="token plain">  </span><span class="token comment">// Use ES6 modules</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  plugins</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token function">multiInput</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token comment">// Keeps the output directory structure same as input</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      relative</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">"output-tmp-rollup/"</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token function">nodeResolve</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain">  </span><span class="token comment">// Allow importing under node_modules</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token function">commonjs</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain">     </span><span class="token comment">// Required for dependencies using commonjs</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain">         </span><span class="token comment">// Allow import data from './data.json'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token function">typescript</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">]</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token punctuation">;</span><span class="token plain"></span></div></code></pre><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="task-runner">Task runner</h2><p class="max-w-xl font-sans mb-paragraph ">Rollup solves many complicated problems for us, but we also need a task runner to combine different tools together. There's no clear line between a module bundler and a task runner though. <a href="https://gruntjs.com/" class="cursor-pointer underline hover:underline ">Grunt</a> or <a href="https://gulpjs.com/" class="cursor-pointer underline hover:underline ">Gulp</a> used to be the de-facto task runners, but Webpack has taken over that territory too in many projects.</p><p class="max-w-xl font-sans mb-paragraph ">For this project though, I'm going to stick with my favorite approach: CLI tools and bash commands. For example HTML validation can be done with:</p><pre class="py-0"><code class="language-bash"><div class="token-line"><span class="token function">find</span><span class="token plain"> ./output/ -name </span><span class="token string">'*.html'</span><span class="token plain"> html-validate </span><span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token plain"> +</span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">The ultimate feature of the approach is that build commands can be copy-pasted to your terminal and easily debugged in steps. The commands tend to get very long, but I don't see that as a huge issue in reality. Long commands can be refactored into separate script files if needed and they focus on doing a single thing well. Bash commands usually operate with regular files, so that's the abstraction you'll mostly need to deal with.</p><p class="max-w-xl font-sans mb-paragraph ">It might get tricky if PowerShell or cmd.exe support is required, but there are tools that make it possible. I created <a href="https://github.com/open-cli-tools/concurrently" class="cursor-pointer underline hover:underline ">concurrently</a> and <a href="https://github.com/open-cli-tools/chokidar-cli" class="cursor-pointer underline hover:underline ">chokidar-cli</a> to help with the cross-platform setup. There are also other great tools such as <a href="https://www.npmjs.com/package/npm-run-all" class="cursor-pointer underline hover:underline ">npm-run-all</a>, <a href="https://www.npmjs.com/package/dotenv" class="cursor-pointer underline hover:underline ">dotenv</a>, and <a href="https://github.com/Qard/onchange" class="cursor-pointer underline hover:underline ">onchange</a>.</p><p class="max-w-xl font-sans mb-paragraph ">This is the stripped-down version of <a href="https://github.com/kimmobrunfeldt/blog/blob/main/package.json" class="cursor-pointer underline hover:underline ">package.json</a> scripts:</p><pre class="py-0"><code class="language-js"><div class="token-line"><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token comment">// ... in package.json</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token string">"scripts"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token string">"build"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">"npm run render &amp;&amp; npm run rollup"</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token string">"rollup"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">"rollup -c rollup.config.js"</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token string">"render"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">"ts-node src/generator/render.tsx"</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// ... many more, such as PostCSS, linter, etc</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="custom-code--templates">Custom code &amp; templates</h2><p class="max-w-xl font-sans mb-paragraph ">We've chosen Rollup for module bundling, and <a href="https://www.keithcirkel.co.uk/how-to-use-npm-as-a-build-tool/" class="cursor-pointer underline hover:underline ">npm run scripts</a> to combine tools together. Next, we'll need custom code that iterates through all React pages and generates their static HTML counterparts. Many details such as rendering MDX pages will be omitted to stick with the topic of Part<!-- -->&nbsp;<!-- -->II.</p><p class="max-w-xl font-sans mb-paragraph ">Everything starts from <a href="https://github.com/kimmobrunfeldt/blog/blob/main/src/generator/render.tsx" class="cursor-pointer underline hover:underline ">src/generator/render.tsx</a>. Rendering has many steps, but these are the relevant ones:</p><ul class="pl-6 list-disc space-y-1"><li class="md:pl-2">Find all entrypoints (i.e. the site pages)</li><li class="md:pl-2">Iterate through entrypoints and render the page templates</li><li class="md:pl-2">Save metadata for entrypoints in <code>site-data.json</code></li></ul><p class="max-w-xl font-sans mb-paragraph ">Let's break the steps down.</p><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="1-find-entrypoints">1. Find entrypoints</h3><p class="max-w-xl font-sans mb-paragraph ">The entrypoints for React page components and their metadata are defined in <a href="https://github.com/kimmobrunfeldt/blog/blob/main/src/pages/_exports.ts" class="cursor-pointer underline hover:underline ">src/pages/_exports.ts</a>. Here's a simplified version of it:</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token keyword">import</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> getData </span><span class="token keyword">as</span><span class="token plain"> getIndexData</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token keyword">default</span><span class="token plain"> </span><span class="token keyword">as</span><span class="token plain"> </span><span class="token maybe-class-name">Index</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"./Index"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> getData </span><span class="token keyword">as</span><span class="token plain"> getPostsData</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token keyword">default</span><span class="token plain"> </span><span class="token keyword">as</span><span class="token plain"> </span><span class="token maybe-class-name">Posts</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"./Posts"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">export</span><span class="token plain"> </span><span class="token keyword">const</span><span class="token plain"> pages </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// Async function in case a page needs to</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// fetch external data.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// Each page has to return title, path, description, etc.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    getData</span><span class="token operator">:</span><span class="token plain"> getIndexData</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token maybe-class-name">Component</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">Index</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// Referred in pageHydrate.tsx:</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// import PageComponent from "src/pages/{{{ fileName }}}";</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    fileName</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">"Index"</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    getData</span><span class="token operator">:</span><span class="token plain"> getPostsData</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token maybe-class-name">Component</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">Posts</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    fileName</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">"Posts"</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">]</span><span class="token punctuation">;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">The list contains all React pages, and which path to serve them from. React page means that the source is a regular React component contrary to e.g. <code>.mdx</code> blog post pages.</p><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="2-iterate-all-pages">2. Iterate all pages</h3><p class="max-w-xl font-sans mb-paragraph ">For each page in the entrypoints, do the following operations:</p><p class="max-w-xl font-sans mb-paragraph "><strong class="font-bold">Call <code>renderToString()</code></strong> to render the React page component as HTML.</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token keyword">const</span><span class="token plain"> html </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">ReactDOMServer</span><span class="token punctuation">.</span><span class="token function">renderToString</span><span class="token punctuation">(</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">page.Component</span><span class="token tag"> </span><span class="token tag attr-name">pageData</span><span class="token tag script language-javascript script-punctuation punctuation">=</span><span class="token tag script language-javascript punctuation">{</span><span class="token tag script language-javascript">pageData</span><span class="token tag script language-javascript punctuation">}</span><span class="token tag"> </span><span class="token tag attr-name">siteData</span><span class="token tag script language-javascript script-punctuation punctuation">=</span><span class="token tag script language-javascript punctuation">{</span><span class="token tag script language-javascript">siteData</span><span class="token tag script language-javascript punctuation">}</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph "><strong class="font-bold">Render <a href="https://github.com/kimmobrunfeldt/blog/blob/main/src/templates/page.html.template" class="cursor-pointer underline hover:underline ">page.html.template</a></strong> with page components's HTML and other metadata. Again, simplified:</p><pre class="py-0"><code class="language-html"><div class="token-line"><span class="token tag punctuation">&lt;</span><span class="token tag">html</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">head</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">title</span><span class="token tag punctuation">&gt;</span><span class="token plain">{{{ title }}}</span><span class="token tag punctuation">&lt;/</span><span class="token tag">title</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">meta</span><span class="token tag"> </span><span class="token tag attr-name">name</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">description</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag attr-name">content</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">{{{ description }}}</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">meta</span><span class="token tag"> </span><span class="token tag attr-name">name</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">keywords</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag attr-name">content</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">{{{ keywords }}}</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">link</span><span class="token tag"> </span><span class="token tag attr-name">defer</span><span class="token tag"> </span><span class="token tag attr-name">rel</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">stylesheet</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag attr-name">href</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">{{{ relativePathToRoot }}}styles.css</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;/</span><span class="token tag">head</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">body</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">div</span><span class="token tag"> </span><span class="token tag attr-name">id</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">react-root</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token plain">{{{ html }}}</span><span class="token tag punctuation">&lt;/</span><span class="token tag">div</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">script</span><span class="token tag"> </span><span class="token tag attr-name">type</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">module</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag attr-name">src</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">{{{ hydrateScriptPath }}}</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token script"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">script</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;/</span><span class="token tag">body</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">html</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph "><strong class="font-bold">Render the page-specific <code>hydrate.tsx</code></strong> using <a href="https://github.com/kimmobrunfeldt/blog/blob/main/src/templates/pageHydrate.tsx.template" class="cursor-pointer underline hover:underline ">pageHydrate.tsx.template</a>. The templates are Mustache-like templates, but with triple braces to avoid conflicts with <code>objectProp={{ key: "val" }}</code> React props.</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token keyword">import</span><span class="token plain"> </span><span class="token maybe-class-name">React</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"react"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token maybe-class-name">ReactDOM</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"react-dom"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> siteData </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"site-data.json"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// Will be replaced with e.g. "src/components/pages/Posts"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token maybe-class-name">PageComponent</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"{{{ pageImportPath }}}"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// Will be replaced with e.g. "/posts/"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> pagePath </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string">"{{{ pagePath }}}"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// Not the best data structure..</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// good thing I don't have 10k blog posts</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> page </span><span class="token operator">=</span><span class="token plain"> siteData</span><span class="token punctuation">.</span><span class="token plain">pages</span><span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">page</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token operator">=&gt;</span><span class="token plain"> page</span><span class="token punctuation">.</span><span class="token plain">data</span><span class="token punctuation">.</span><span class="token plain">path </span><span class="token operator">===</span><span class="token plain"> pagePath</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token dom variable">window</span><span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"load"</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token operator">=&gt;</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token maybe-class-name">ReactDOM</span><span class="token punctuation">.</span><span class="token function">hydrate</span><span class="token punctuation">(</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag class-name">PageComponent</span><span class="token tag"> </span><span class="token tag attr-name">pageData</span><span class="token tag script language-javascript script-punctuation punctuation">=</span><span class="token tag script language-javascript punctuation">{</span><span class="token tag script language-javascript">page</span><span class="token tag script language-javascript punctuation">.</span><span class="token tag script language-javascript">data</span><span class="token tag script language-javascript punctuation">}</span><span class="token tag"> </span><span class="token tag attr-name">siteData</span><span class="token tag script language-javascript script-punctuation punctuation">=</span><span class="token tag script language-javascript punctuation">{</span><span class="token tag script language-javascript">siteData</span><span class="token tag script language-javascript punctuation">}</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token dom variable">document</span><span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">"react-root"</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">The hydration script will be first saved as a TypeScript file to <code>output-tmp-rollup/&lt;page&gt;/hydrate.tsx</code>. The render script keeps the directory structure correct, so it matches the paths defined in pages' metadata. The <code>output-tmp-rollup/**/*.tsx</code> will finally be passed to Rollup, which outputs corresponding <code>output/**/*.js</code> files.</p><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="3-save-metadata-to-site-datajson">3. Save metadata to site-data.json</h3><p class="max-w-xl font-sans mb-paragraph ">As seen in <code>pageHydrate.tsx.template</code>, site-wide data is imported from <code>site-data.json</code>. The data is used to for example render a list of all posts. The JSON data is saved in a single file, that can be cached and shared by all pages in the site.</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token keyword">import</span><span class="token plain"> fs </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"fs"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> pages </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"src/pages/_exports"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> siteData </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword">await</span><span class="token plain"> </span><span class="token function">mapAsync</span><span class="token punctuation">(</span><span class="token plain">pages</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token parameter">page</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token operator">=&gt;</span><span class="token plain"> page</span><span class="token punctuation">.</span><span class="token function">getData</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">await</span><span class="token plain"> fs</span><span class="token punctuation">.</span><span class="token plain">promises</span><span class="token punctuation">.</span><span class="token function">writeFile</span><span class="token punctuation">(</span><span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token plain">siteData</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">"site-data.json"</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  encoding</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">"utf-8"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div></code></pre><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="putting-everything-together">Putting everything together</h2><p class="max-w-xl font-sans mb-paragraph ">With these building blocks, we are able to convert React pages into static HTML counterparts. To recap, <code>npm run build</code> will:</p><ol class="pl-6 list-decimal space-y-1"><li class="md:pl-2">Render all pages with <code>src/generator/render.tsx</code></li><li class="md:pl-2">Run Rollup to transpile <code>output-tmp-rollup/**/*.tsx</code> files into <code>output/**/*.js</code></li><li class="md:pl-2">Execute other build steps (copy static files, PostCSS, etc)</li></ol><div class=" my-12"><a href="https://kimmo.blog/content-assets/3/dir-structure-build.svg" rel="noreferrer" target="_blank"><img class="mx-auto w-full" alt="Directory structures" src="https://kimmo.blog/content-assets/3/dir-structure-build.svg"></a></div><p class="max-w-xl font-sans mb-paragraph ">The build flow is simple in theory, but there are a huge amount of details that end up making the process complicated.</p></div></body></html>]]></content:encoded><content:media url="https://kimmo.blog/content-assets/3/cover.png" type="image/jpeg" medium="image" xmlns:media="http://search.yahoo.com/mrss/"/></item><item><title><![CDATA[DIY Gatsby: MDX]]></title><description><![CDATA[Part III of a series where we'll look into how this blog was made. The third part covers how MDX rendering works.
]]></description><link>https://kimmo.blog/posts/4-diy-gatsby-part-iii/</link><guid isPermaLink="false">https://kimmo.blog/posts/4-diy-gatsby-part-iii/</guid><category><![CDATA[static site generator]]></category><category><![CDATA[react]]></category><category><![CDATA[server-side rendering]]></category><category><![CDATA[mdx]]></category><category><![CDATA[gatsby]]></category><pubDate>Fri, 03 Sep 2021 00:00:00 GMT</pubDate><enclosure url="https://kimmo.blog/content-assets/4/cover.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<html><head></head><body><div data-reactroot=""><p class="max-w-xl font-sans mb-paragraph "><em class="italic">This is Part<!-- -->&nbsp;<!-- -->III of a series where we'll look into how kimmo.blog works. Part<!-- -->&nbsp;<!-- -->III covers MDX rendering.</em></p><p class="max-w-xl font-sans mb-paragraph "><em class="italic">Motivation has been covered in <a href="https://kimmo.blog/posts/1-writing-or-coding/" class="cursor-pointer underline hover:underline ">Writing, or coding</a>.</em></p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="what-is-mdx">What is MDX</h2><p class="max-w-xl font-sans mb-paragraph "><a href="https://mdxjs.com/" class="cursor-pointer underline hover:underline ">MDX</a> is an amazing markup format. It allows familiar authoring experience using markdown syntax – with the capability of adding custom React components along with the text.</p><p class="max-w-xl font-sans mb-paragraph ">For example this source</p><pre class="py-0"><code class="language-markdown"><div class="token-line"><span class="token bold punctuation">**</span><span class="token bold content">Here's</span><span class="token bold punctuation">**</span><span class="token plain"> an </span><span class="token italic punctuation">_</span><span class="token italic content">example</span><span class="token italic punctuation">_</span><span class="token plain"> of a </span><span class="token url">[</span><span class="token url content">toast</span><span class="token url">](</span><span class="token url">https://ux.stackexchange.com/questions/11998/what-is-a-toast-notification</span><span class="token url">)</span><span class="token plain">:</span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">&lt;ButtonLink onClick={() =&gt; info('This is amazing!')}&gt;Spawn a toast</span><span class="token tag punctuation">&lt;/</span><span class="token tag">ButtonLink</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">would render:</p><p class="max-w-xl font-sans mb-paragraph "><strong class="font-bold">Here's</strong> an <em class="italic">example</em> of a <a href="https://ux.stackexchange.com/questions/11998/what-is-a-toast-notification" class="cursor-pointer underline hover:underline ">toast</a>:</p><div class="mb-paragraph"><a class="inline-block cursor-pointer underline-effect-button bg-transparent text-rust-5 font-bold border border-rust-5 rounded focus:outline-none active:outline-none dark:text-rust-6 dark:active:text-rust-6 dark:border-rust-6   py-2 px-4">Spawn a toast</a></div><p class="max-w-xl font-sans mb-paragraph ">All this could be achieved with good old React components, but it's just not as smooth of a writing experience. Paragraphs would need to be wrapped in <code>&lt;p&gt;</code> elements, links would become cumbersome to type, and <em class="italic">blah</em>.</p><p class="max-w-xl font-sans mb-paragraph ">Writing blog posts in React would make the site generator simpler, but we're already on the over-engineering train so let's continue.</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="how-mdx-works">How MDX works</h2><p class="max-w-xl font-sans mb-paragraph ">While MDX is an incredible authoring experience, it comes at the price of complexity when done right. By right, I mean with good performance.</p><p class="max-w-xl font-sans mb-paragraph ">It is possible to greatly simplify the rendering, if performance is not a concern. MDX text could be sent as a raw string to the browser and it would do the heavy rendering with e.g. <a href="https://mdxjs.com/advanced/runtime#mdx-jsruntime" class="cursor-pointer underline hover:underline ">@mdx-js/runtime</a>. It would make the build pipeline simpler, but other aspects worse:</p><blockquote class="text-gray-6 dark:text-gray-5 italic py-2 px-7 my-6 text-center text-lg md:relative md:-left-6"><p class="max-w-xl font-sans mb-paragraph ">"warning: this is not the preferred way to use MDX since it introduces a substantial amount of overhead and dramatically increases bundle sizes. It must not be used with user input that isn’t sandboxed."</p></blockquote><p class="max-w-xl font-sans mb-paragraph ">This blog uses <a href="https://github.com/hashicorp/next-mdx-remote/" class="cursor-pointer underline hover:underline ">next-mdx-remote</a> library to deal with MDX rendering. It's been designed for Next.js, but it also works well for our use case.</p><p class="max-w-xl font-sans mb-paragraph ">To speed things up in the browser, next-mdx-remote compiles the MDX to minified JavaScript server-side. The <a href="https://github.com/hashicorp/next-mdx-remote/blob/3.0.4/src/serialize.ts#L57" class="cursor-pointer underline hover:underline ">serialize</a> method does this using <a href="https://github.com/mdx-js/mdx/blob/v2.0.0-next.9/docs/getting-started/index.mdx#do-it-yourself" class="cursor-pointer underline hover:underline ">@mdx-js/mdx</a> and <a href="https://github.com/evanw/esbuild" class="cursor-pointer underline hover:underline ">esbuild</a> under the hood.</p><p class="max-w-xl font-sans mb-paragraph ">This is how the previous example would be serialized:</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token keyword">import</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> serialize </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"next-mdx-remote/serialize"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> content </span><span class="token operator">=</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string string"></span></div><div class="token-line"><span class="token template-string string">**Here's** an _example_ of a [toast](https://ux.stackexchange.com/questions/11998/what-is-a-toast-notification):</span></div><div class="token-line"><span class="token template-string string">
</span></div><div class="token-line"><span class="token template-string string">&lt;ButtonLink onClick={() =&gt; info('This is amazing!')}&gt;Spawn a toast&lt;/ButtonLink&gt;</span></div><div class="token-line"><span class="token template-string string"></span><span class="token template-string template-punctuation string">`</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> compiledSource </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword">await</span><span class="token plain"> </span><span class="token function">serialize</span><span class="token punctuation">(</span><span class="token plain">content</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">The <code>compiledSource</code> is a string that contains the compiled JavaScript. Here's the code unminified and simplified:</p><pre class="py-0"><code class="language-js"><div class="token-line"><span class="token keyword">function</span><span class="token plain"> </span><span class="token function maybe-class-name">MDXContent</span><span class="token punctuation">(</span><span class="token parameter">n</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">var</span><span class="token plain"> e </span><span class="token operator">=</span><span class="token plain"> n</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      components</span><span class="token operator">:</span><span class="token plain"> o</span></div><div class="token-line"><span class="token plain">    </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> e</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    t </span><span class="token operator">=</span><span class="token plain"> </span><span class="token function">m</span><span class="token punctuation">(</span><span class="token plain">e</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token string">"components"</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword control-flow">return</span><span class="token plain"> </span><span class="token function">mdx</span><span class="token punctuation">(</span><span class="token maybe-class-name">MDXLayout</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token function">c</span><span class="token punctuation">(</span><span class="token function">a</span><span class="token punctuation">(</span><span class="token function">a</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span><span class="token plain"> layoutProps</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"> t</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    components</span><span class="token operator">:</span><span class="token plain"> o</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    mdxType</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">"MDXLayout"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token function">mdx</span><span class="token punctuation">(</span><span class="token string">"p"</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token keyword null nil">null</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token function">mdx</span><span class="token punctuation">(</span><span class="token string">"strong"</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    parentName</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">"p"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">"Here's"</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">" an "</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token function">mdx</span><span class="token punctuation">(</span><span class="token string">"em"</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    parentName</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">"p"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">"example"</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">" of a "</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token function">mdx</span><span class="token punctuation">(</span><span class="token string">"a"</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token function">a</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    parentName</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">"p"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    href</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">"https://ux.stackexchange.com/questions/11998/what-is-a-toast-notification"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">"a toast"</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">":"</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token function">mdx</span><span class="token punctuation">(</span><span class="token maybe-class-name">ButtonLink</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token function-variable function">onClick</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token arrow operator">=&gt;</span><span class="token plain"> </span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"This is amazing!"</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    mdxType</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">"ButtonLink"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">"Spawn a toast"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">In the browser, next-mdx-remote <a href="https://github.com/hashicorp/next-mdx-remote/blob/3.0.4/src/index.tsx#L97" class="cursor-pointer underline hover:underline ">evals</a> the code with <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/construct" class="cursor-pointer underline hover:underline ">Reflect.construct()</a>, and renders the MDXContent:</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token tag punctuation">&lt;</span><span class="token tag class-name">MDX.MDXProvider</span><span class="token tag"> </span><span class="token tag attr-name">components</span><span class="token tag script language-javascript script-punctuation punctuation">=</span><span class="token tag script language-javascript punctuation">{</span><span class="token tag script language-javascript">components</span><span class="token tag script language-javascript punctuation">}</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag class-name">MDXContent</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;/</span><span class="token tag class-name">MDX.MDXProvider</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div></code></pre><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="how-the-blog-renders-posts">How the blog renders posts</h2><p class="max-w-xl font-sans mb-paragraph ">The high-level flow of rendering <code>.mdx</code> pages is similar to React pages, but has more steps:</p><ul class="pl-6 list-disc space-y-1"><li class="md:pl-2">Find all <code>.mdx</code> blog posts</li><li class="md:pl-2">For each blog post:<ul class="pl-6 list-disc space-y-1"><li class="md:pl-2">Parse metadata from <a href="https://github.com/jonschlinkert/gray-matter" class="cursor-pointer underline hover:underline ">front matter</a></li><li class="md:pl-2">Render <a href="https://github.com/kimmobrunfeldt/blog/blob/main/src/templates/PostPage.tsx.template" class="cursor-pointer underline hover:underline ">PostPage.tsx.template</a></li><li class="md:pl-2">Render <a href="https://github.com/kimmobrunfeldt/blog/blob/main/src/templates/pageHydrate.tsx.template" class="cursor-pointer underline hover:underline ">pageHydrate.tsx.template</a></li><li class="md:pl-2">Render <a href="https://github.com/kimmobrunfeldt/blog/blob/main/src/templates/page.html.template" class="cursor-pointer underline hover:underline ">page.html.template</a> with the HTML generated from MDX and other metadata</li><li class="md:pl-2">Save a <code>compiledSource.txt</code> file which contains the MDX content as minified JavaScript.</li></ul></li></ul><p class="max-w-xl font-sans mb-paragraph ">We'll soon understand why <code>.txt</code> extension is used. Let's go through the templates one by one.</p><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="postpagetsxtemplate">PostPage.tsx.template</h3><p class="max-w-xl font-sans mb-paragraph ">The <a href="https://github.com/kimmobrunfeldt/blog/blob/main/src/templates/PostPage.tsx.template" class="cursor-pointer underline hover:underline ">template</a> looks like this:</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token keyword">import</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> </span><span class="token maybe-class-name">PostLayout</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"src/components/PostLayout"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> </span><span class="token maybe-class-name">MDXRemote</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"src/components/MDXRemote"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// Will be replaced with "./compiledSource.txt"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> compiledSource </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"{{{ compiledSourcePath }}}"</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">export</span><span class="token plain"> </span><span class="token keyword">default</span><span class="token plain"> </span><span class="token keyword">function</span><span class="token plain"> </span><span class="token function maybe-class-name">PageComponent</span><span class="token punctuation">(</span><span class="token parameter">props</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">return</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag class-name">PostLayout</span><span class="token tag"> </span><span class="token tag attr-name">siteData</span><span class="token tag script language-javascript script-punctuation punctuation">=</span><span class="token tag script language-javascript punctuation">{</span><span class="token tag script language-javascript">props</span><span class="token tag script language-javascript punctuation">.</span><span class="token tag script language-javascript">siteData</span><span class="token tag script language-javascript punctuation">}</span><span class="token tag"> </span><span class="token tag attr-name">data</span><span class="token tag script language-javascript script-punctuation punctuation">=</span><span class="token tag script language-javascript punctuation">{</span><span class="token tag script language-javascript">props</span><span class="token tag script language-javascript punctuation">.</span><span class="token tag script language-javascript">pageData</span><span class="token tag script language-javascript punctuation">}</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token tag punctuation">&lt;</span><span class="token tag class-name">MDXRemote</span><span class="token tag"> </span><span class="token tag attr-name">compiledSource</span><span class="token tag script language-javascript script-punctuation punctuation">=</span><span class="token tag script language-javascript punctuation">{</span><span class="token tag script language-javascript">compiledSource</span><span class="token tag script language-javascript punctuation">}</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;/</span><span class="token tag class-name">PostLayout</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">The purpose of the rendered file is to represent the React component for a blog post page. The template will be rendered to a verbosely named file: <code>output/posts/&lt;slug&gt;/&lt;slug&gt;-post.tsx</code>.</p><p class="max-w-xl font-sans mb-paragraph ">It's easier to debug the build process when unique names are used instead of tens of "post.tsx" files.</p><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="pagehydratetsxtemplate">pageHydrate.tsx.template</h3><p class="max-w-xl font-sans mb-paragraph ">The same template that we used to render React pages in <a href="https://kimmo.blog/posts/3-diy-gatsby-part-ii/" class="cursor-pointer underline hover:underline ">Part&nbsp;II</a>. In React pages, the import refers to a page component in the source code.</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token keyword">import</span><span class="token plain"> </span><span class="token maybe-class-name">PageComponent</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"src/pages/NotFound404"</span><span class="token punctuation">;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">With MDX pages on the other hand, the <code>PageComponent</code> is imported from the dynamically rendered post page file:</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token keyword">import</span><span class="token plain"> </span><span class="token maybe-class-name">PageComponent</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"./1-writing-or-coding-post"</span><span class="token punctuation">;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">The template is saved to <code>output/posts/&lt;slug&gt;/&lt;slug&gt;-post-hydrate.tsx</code>.</p><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="pagehtmltemplate">page.html.template</h3><p class="max-w-xl font-sans mb-paragraph ">Again, the same template as for React pages. The MDX content will be rendered as HTML in <code>&lt;body&gt;</code>. Blog post title, description, and tags from the front matter are rendered into <code>&lt;head&gt;</code>:</p><pre class="py-0"><code class="language-html"><div class="token-line"><span class="token tag punctuation">&lt;</span><span class="token tag">html</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">head</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">title</span><span class="token tag punctuation">&gt;</span><span class="token plain">Writing, or coding - kimmo.blog</span><span class="token tag punctuation">&lt;/</span><span class="token tag">title</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">meta</span><span class="token tag"> </span><span class="token tag attr-name">name</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">description</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag attr-name">content</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">Do I want to write content, or do I want to code a blog?</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">meta</span><span class="token tag"> </span><span class="token tag attr-name">name</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">keywords</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag attr-name">content</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">meta,blog,content creation</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;/</span><span class="token tag">head</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;</span><span class="token tag">body</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">div</span><span class="token tag"> </span><span class="token tag attr-name">id</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">react-root</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token comment">&lt;!-- MDX blog page as HTML --&gt;</span><span class="token tag punctuation">&lt;/</span><span class="token tag">div</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token tag punctuation">&lt;</span><span class="token tag">script</span><span class="token tag"> </span><span class="token tag attr-name">type</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">module</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag attr-name">src</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">./1-writing-or-coding-post-hydrate.js</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token script"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">script</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token tag punctuation">&lt;/</span><span class="token tag">body</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">html</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div></code></pre><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="compiledsourcetxt">compiledSource.txt</h3><p class="max-w-xl font-sans mb-paragraph ">As mentioned, this file contains the MDX as minified JavaScript.</p><p class="max-w-xl font-sans mb-paragraph "><em class="italic">Why does it have a <code>.txt</code> extension then?</em></p><p class="max-w-xl font-sans mb-paragraph ">We need to be able to transfer the code exactly as-is to the browser, so that it can be passed as a string to <a href="https://github.com/hashicorp/next-mdx-remote#apis" class="cursor-pointer underline hover:underline ">MDXRemote</a>. A good way to do this is using a <a href="https://github.com/TrySound/rollup-plugin-string" class="cursor-pointer underline hover:underline ">rollup string plugin</a>. The plugin has been <a href="https://github.com/kimmobrunfeldt/blog/blob/3f15ad33ed431486136b612a32a1b1339596b28b/rollup.config.js#L28" class="cursor-pointer underline hover:underline ">configured</a> so that a TypeScript code can import any <code>.txt</code> file, and the exact contents will be assigned to a variable.</p><p class="max-w-xl font-sans mb-paragraph ">This functionality used to be just a simple text replacement directly into code, but that broke with special characters and line breaks.</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="to-recap">To recap</h2><div class="select-none  mt-12 mb-16"><ul class="flex flex-row justify-end mb-2"><li><button type="button" class="cursor-not-allowed text-gray-3 dark:text-gray-7 block top-[1px]" title="Previous" aria-label="Previous animation state" disabled=""><span style="font-size:1.3em"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="box-content p-2" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M8.854 4.854l.353-.354l-.707-.707l-.354.353l.708.708zM5.5 7.5l-.354-.354l-.353.354l.353.354L5.5 7.5zm2.646 3.354l.354.353l.707-.707l-.353-.354l-.708.708zm0-6.708l-3 3l.708.708l3-3l-.708-.708zm-3 3.708l3 3l.708-.708l-3-3l-.708.708zM7.5 14A6.5 6.5 0 0 1 1 7.5H0A7.5 7.5 0 0 0 7.5 15v-1zM14 7.5A6.5 6.5 0 0 1 7.5 14v1A7.5 7.5 0 0 0 15 7.5h-1zM7.5 1A6.5 6.5 0 0 1 14 7.5h1A7.5 7.5 0 0 0 7.5 0v1zm0-1A7.5 7.5 0 0 0 0 7.5h1A6.5 6.5 0 0 1 7.5 1V0z" fill="currentColor"></path></g></svg></span></button></li><li><button type="button" class="text-rust-6 hover:text-rust-5 active:text-rust-7 dark:text-rust-6 dark:active:text-rust-5 block top-[1px]" title="Back to start" aria-label="Back to start"><span style="font-size:1.3em"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="box-content p-2" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M14 8.495v-.5h-1v.5h1zM7.5 2.999H8v-1h-.5v1zm1-.5l.353.353l.354-.353l-.354-.354l-.353.354zM13 8.495a5.499 5.499 0 0 1-5.5 5.496v1c3.589 0 6.5-2.909 6.5-6.496h-1zM7.5 13.99A5.499 5.499 0 0 1 2 8.495H1a6.499 6.499 0 0 0 6.5 6.496v-1zM2 8.495a5.499 5.499 0 0 1 5.5-5.496v-1A6.499 6.499 0 0 0 1 8.495h1zM6.147.854l2 1.998l.706-.707l-2-1.999l-.706.708zm2 1.291l-2 1.999l.706.707l2-1.999l-.706-.707z" fill="currentColor"></path></g></svg><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="box-content p-2 hidden" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M6.146 10.146l-.353.354l.707.707l.354-.353l-.708-.708zM9.5 7.5l.354.354l.353-.354l-.353-.354L9.5 7.5zM6.854 4.146L6.5 3.793l-.707.707l.353.354l.708-.708zm0 6.708l3-3l-.708-.708l-3 3l.708.708zm3-3.708l-3-3l-.708.708l3 3l.708-.708zM7.5 1A6.5 6.5 0 0 1 14 7.5h1A7.5 7.5 0 0 0 7.5 0v1zM1 7.5A6.5 6.5 0 0 1 7.5 1V0A7.5 7.5 0 0 0 0 7.5h1zM7.5 14A6.5 6.5 0 0 1 1 7.5H0A7.5 7.5 0 0 0 7.5 15v-1zm0 1A7.5 7.5 0 0 0 15 7.5h-1A6.5 6.5 0 0 1 7.5 14v1z" fill="currentColor"></path></g></svg></span></button></li></ul><div class=" mx-auto"><noscript>Animation requires JavaScript</noscript><img alt="MDX rendering" src="https://kimmo.blog/content-assets/4/dir-structure-build-mdx.svg"></div></div><p class="max-w-xl font-sans mb-paragraph ">Each <code>.mdx</code> blog post file ends up creating quite some files in the output directory. First TypeScript files and <code>compileSource.txt</code> are rendered, then Rollup transpiles the source code into JS bundles, and finally static assets are copied.</p></div></body></html>]]></content:encoded><content:media url="https://kimmo.blog/content-assets/4/cover.png" type="image/jpeg" medium="image" xmlns:media="http://search.yahoo.com/mrss/"/></item><item><title><![CDATA[DIY Gatsby: Worth it?]]></title><description><![CDATA[Part IV of a series where we'll look into how this blog was made. This part focuses on the pros and cons of the DIY build system.
]]></description><link>https://kimmo.blog/posts/5-diy-gatsby-part-iv/</link><guid isPermaLink="false">https://kimmo.blog/posts/5-diy-gatsby-part-iv/</guid><category><![CDATA[static site generator]]></category><category><![CDATA[react]]></category><category><![CDATA[server-side rendering]]></category><category><![CDATA[gatsby]]></category><pubDate>Wed, 22 Sep 2021 00:00:00 GMT</pubDate><enclosure url="https://kimmo.blog/content-assets/5/cover.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<html><head></head><body><div data-reactroot=""><p class="max-w-xl font-sans mb-paragraph "><em class="italic">This is Part<!-- -->&nbsp;<!-- -->IV of a series where we'll look into how kimmo.blog works. Part<!-- -->&nbsp;<!-- -->IV focuses on the pros and cons of the DIY build system.</em></p><p class="max-w-xl font-sans mb-paragraph "><em class="italic">Motivation has been covered in <a href="https://kimmo.blog/posts/1-writing-or-coding/" class="cursor-pointer underline hover:underline ">Writing, or coding</a>.</em></p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="diy-vs-gatsby">DIY vs Gatsby</h2><p class="max-w-xl font-sans mb-paragraph ">The disclaimers in <a href="https://kimmo.blog/posts/2-diy-gatsby-part-i/" class="cursor-pointer underline hover:underline ">Part&nbsp;I</a> briefly mentioned this already, but it's worth recapping. <a href="https://www.gatsbyjs.com/" class="cursor-pointer underline hover:underline ">Gatsby</a> is a production-ready tool and in this series we built only a part of it.</p><p class="max-w-xl font-sans mb-paragraph ">Let's look at a classic feature comparison table.</p><table><thead><tr><th>Feature</th><th align="center">DIY</th><th align="center">Gatsby</th></tr></thead><tbody><tr><td>Site generator</td><td align="center"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="inline-block " width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M1 7l4.5 4.5L14 3" stroke="currentColor" stroke-linecap="square"></path></g></svg></td><td align="center"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="inline-block " width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M1 7l4.5 4.5L14 3" stroke="currentColor" stroke-linecap="square"></path></g></svg></td></tr><tr><td>Great end-user performance</td><td align="center"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="inline-block " width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M1 7l4.5 4.5L14 3" stroke="currentColor" stroke-linecap="square"></path></g></svg></td><td align="center"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="inline-block " width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M1 7l4.5 4.5L14 3" stroke="currentColor" stroke-linecap="square"></path></g></svg></td></tr><tr><td>MDX</td><td align="center"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="inline-block " width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M1 7l4.5 4.5L14 3" stroke="currentColor" stroke-linecap="square"></path></g></svg></td><td align="center"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="inline-block " width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M1 7l4.5 4.5L14 3" stroke="currentColor" stroke-linecap="square"></path></g></svg></td></tr><tr><td>Frontend routing</td><td align="center"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="inline-block " width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M4.5 4.5l6 6m-6 0l6-6" stroke="currentColor"></path></g></svg></td><td align="center"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="inline-block " width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M1 7l4.5 4.5L14 3" stroke="currentColor" stroke-linecap="square"></path></g></svg></td></tr><tr><td>Custom data sources</td><td align="center"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="inline-block " width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M4.5 4.5l6 6m-6 0l6-6" stroke="currentColor"></path></g></svg></td><td align="center"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="inline-block " width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M1 7l4.5 4.5L14 3" stroke="currentColor" stroke-linecap="square"></path></g></svg></td></tr><tr><td>Documentation</td><td align="center"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="inline-block " width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M4.5 4.5l6 6m-6 0l6-6" stroke="currentColor"></path></g></svg></td><td align="center"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="inline-block " width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M1 7l4.5 4.5L14 3" stroke="currentColor" stroke-linecap="square"></path></g></svg></td></tr><tr><td>Community</td><td align="center"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="inline-block " width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M4.5 4.5l6 6m-6 0l6-6" stroke="currentColor"></path></g></svg></td><td align="center"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="inline-block " width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M1 7l4.5 4.5L14 3" stroke="currentColor" stroke-linecap="square"></path></g></svg></td></tr></tbody></table><p class="max-w-xl font-sans mb-paragraph ">The DIY solution has a similar core functionality, but lacks in some areas. Nevertheless, the tool works and successfully builds this blog in a similar way Gatsby would. The solutions are similar, but it's not a direct comparison.</p><p class="max-w-xl font-sans mb-paragraph ">Let's go through the good and the bad.</p><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="mt-32 mb-20 mx-auto text-rust-5" width="55px" height="55px" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M4.9 8.7l-.3-.4l-.8.6l.3.4l.8-.6zm6 .6l.3-.4l-.8-.6l-.3.4l.8.6zM7.5 14A6.5 6.5 0 0 1 1 7.5H0A7.5 7.5 0 0 0 7.5 15v-1zM14 7.5A6.5 6.5 0 0 1 7.5 14v1A7.5 7.5 0 0 0 15 7.5h-1zM7.5 1A6.5 6.5 0 0 1 14 7.5h1A7.5 7.5 0 0 0 7.5 0v1zm0-1A7.5 7.5 0 0 0 0 7.5h1A6.5 6.5 0 0 1 7.5 1V0zM4 6h1V5H4v1zm6 0h1V5h-1v1zm.1 2.7a3.25 3.25 0 0 1-5.2 0l-.8.6c1.7 2.267 5.1 2.267 6.8 0l-.8-.6z" fill="currentColor"></path></g></svg><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="good-1-technical-simplicity">Good #1: Technical simplicity</h2><p class="max-w-xl font-sans mb-paragraph ">My thoughts are inevitably biased because I've incrementally gathered knowledge of the system while developing the blog. Knowing that I'd still claim that the build system is pretty simple considering how much it does.</p><p class="max-w-xl font-sans mb-paragraph ">The simplicity has been achieved by keeping the number of abstraction levels to a minimum. A few examples:</p><ul class="pl-6 list-disc space-y-1"><li class="md:pl-2">Build steps operate on directories and files</li><li class="md:pl-2">Each build step is a narrowly scoped CLI command</li><li class="md:pl-2">ES6 modules are leveraged to keep bundling simple</li></ul><p class="max-w-xl font-sans mb-paragraph ">Part of the minimalism can be attributed to the lack of varying use cases. Gatsby supports thousands of use cases, while the custom-tailored tooling caters just this blog.</p><p class="max-w-xl font-sans mb-paragraph ">Something to keep in mind is that simple doesn't equal easy. While the tooling is simple, it doesn't automatically mean a better choice for a new website. The lack of documentation and community around the tooling would likely total to a net negative in comparison to widely used tools like <a href="https://www.gatsbyjs.com/" class="cursor-pointer underline hover:underline ">Gatsby</a> or <a href="https://github.com/react-static/react-static" class="cursor-pointer underline hover:underline ">react-static</a>.</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="good-2-no-graphql-in-simple-workflows">Good #2: No GraphQL in simple workflows</h2><p class="max-w-xl font-sans mb-paragraph ">Some simple tasks are overly complicated to achieve in Gatsby. The reason for that in my opinion is the GraphQL layer. It's applicable when fetching more complex data from a content management system but not so much for retrieving local .mdx files.</p><p class="max-w-xl font-sans mb-paragraph ">The <a href="https://www.gatsbyjs.com/plugins/gatsby-plugin-mdx/" class="cursor-pointer underline hover:underline ">documentation</a> for using MDX content in Gatsby starts with configuring the <code>gatsby-source-filesystem</code> plugin. After that, you'll spend time reading the documentation for accessing the file system via GraphQL queries. It's not a very common pattern after all.</p><p class="max-w-xl font-sans mb-paragraph ">With the DIY system, you access the files as you'd normally do:</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token keyword">const</span><span class="token plain"> files </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword">await</span><span class="token plain"> </span><span class="token function">glob</span><span class="token punctuation">(</span><span class="token string">"posts/*.mdx"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> promises </span><span class="token operator">=</span><span class="token plain"> files</span><span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">name</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token operator">=&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  fs</span><span class="token punctuation">.</span><span class="token plain">promises</span><span class="token punctuation">.</span><span class="token function">readFile</span><span class="token punctuation">(</span><span class="token plain">name</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    encoding</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">'utf8'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> contents </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword">await</span><span class="token plain"> </span><span class="token known-class-name class-name">Promise</span><span class="token punctuation">.</span><span class="token function">all</span><span class="token punctuation">(</span><span class="token plain">promises</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">The argument might not be a fair one, since it's completely understandable why Gatsby does this. They rely on GraphQL as the single tech for accessing data. It's a smart choice for the average use case — accessing data from varying sources.</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="good-3-skipping-an-abstraction-layer">Good #3: Skipping an abstraction layer</h2><p class="max-w-xl font-sans mb-paragraph ">Gatsby has over <a href="https://www.gatsbyjs.com/plugins" class="cursor-pointer underline hover:underline ">2500 plugins</a>. Many of them are tremendous and greatly simplify otherwise complicated workflows. For example <code>gatsby-plugin-image</code> adds responsive images automatically on your website.</p><p class="max-w-xl font-sans mb-paragraph ">The downside of plugins is that they introduce an unnecessary Gatsby-specific abstraction on top of the original problem. For example, markdown processing is already complicated enough with <a href="https://github.com/remarkjs/remark" class="cursor-pointer underline hover:underline ">remark</a> and <a href="https://github.com/unifiedjs/unified" class="cursor-pointer underline hover:underline ">unified</a>, but Gatsby makes it even more complicated with its own set of <a href="https://www.gatsbyjs.com/plugins/gatsby-plugin-sitemap/?=remark" class="cursor-pointer underline hover:underline ">plugins</a>.</p><p class="max-w-xl font-sans mb-paragraph ">Even in a Gatsby project, I prefer to solve some problems natively instead of using a plugin. For example, it might be simpler to just add a <a href="https://medium.com/swlh/are-you-using-svg-favicons-yet-a-guide-for-modern-browsers-836a6aace3df" class="cursor-pointer underline hover:underline ">favicon.svg</a> in the <a href="https://www.gatsbyjs.com/docs/how-to/images-and-media/static-folder/" class="cursor-pointer underline hover:underline ">static directory</a>, instead of using a plugin for that.</p><p class="max-w-xl font-sans mb-paragraph ">In the DIY setup, we skip an abstraction layer. The code directly uses <a href="https://www.npmjs.com/package/rss" class="cursor-pointer underline hover:underline ">rss</a>, <a href="https://github.com/remarkjs/remark" class="cursor-pointer underline hover:underline ">remark</a>, and other libraries. There's no plugin API or configuration format in the middle to learn.</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="good-4-rollup-bundling">Good #4: Rollup bundling</h2><p class="max-w-xl font-sans mb-paragraph ">By leveraging Rollup and <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules" class="cursor-pointer underline hover:underline ">ES6 modules</a>, the bundling becomes intuitive. Instead of a single large bundle.js, source code and its dependencies are bundled into separate cacheable files.</p><p class="max-w-xl font-sans mb-paragraph ">ES6 modules are used in the development environment as well as in the production site. Because this is a developer-focused blog, I can just ignore the browsers that <a href="https://caniuse.com/?search=esm" class="cursor-pointer underline hover:underline ">don't support<!-- --> <abbr title="ES6 Modules">ESM</abbr></a>.</p><p class="max-w-xl font-sans mb-paragraph ">The screenshots below show which JS files are loaded for kimmo.blog in comparison to <a href="https://gatsbystarterblogsource.gatsbyjs.io/" class="cursor-pointer underline hover:underline ">Gatsby starter blog</a>.</p><div class=" my-12"><a href="https://kimmo.blog/content-assets/5/devtools.jpg" rel="noreferrer" target="_blank"><img class="mx-auto w-full" alt="Chrome Devtools for DIY solution" src="https://kimmo.blog/content-assets/5/devtools.jpg"></a><p class="italic text-sm mt-4 text-center">kimmo.blog</p></div><div class=" my-12"><a href="https://kimmo.blog/content-assets/5/devtools-gatsby.jpg" rel="noreferrer" target="_blank"><img class="mx-auto w-full" alt="Chrome Devtools for Gatsby site" src="https://kimmo.blog/content-assets/5/devtools-gatsby.jpg"></a><p class="italic text-sm mt-4 text-center">Gatsby starter blog</p></div><p class="max-w-xl font-sans mb-paragraph ">Although the starter blog doesn't have <a href="https://github.com/gatsbyjs/gatsby-starter-blog/tree/master/src/components" class="cursor-pointer underline hover:underline ">many components</a>, Gatsby uses Webpack to bundle components into a few different bundles. This process leads to fewer individual JS files but makes the bundles quite cryptic. Gatsby's <a href="https://www.gatsbyjs.com/docs/how-code-splitting-works/" class="cursor-pointer underline hover:underline ">documentation</a> explains how bundles are formed in great detail.</p><p class="max-w-xl font-sans mb-paragraph ">The DIY version on the other hand has a predictable mapping from source code to built version of the site:</p><ul class="pl-6  space-y-1"><li class="md:pl-2 flex flex-row items-start space-x-4"><span><code class="language-no-highlight"><div class="token-line"><span class="token plain">PostSummary.tsx</span></div></code> → <code class="language-no-highlight"><div class="token-line"><span class="token plain">/PostSummary.js</span></div></code></span></li><li class="md:pl-2 flex flex-row items-start space-x-4"><span><code class="language-no-highlight"><div class="token-line"><span class="token plain">Button.tsx</span></div></code> → <code class="language-no-highlight"><div class="token-line"><span class="token plain">/Button.js</span></div></code></span></li><li class="md:pl-2 flex flex-row items-start space-x-4"><span><code class="language-no-highlight"><div class="token-line"><span class="token plain">import _ from "lodash"</span></div></code> → <code class="language-no-highlight"><div class="token-line"><span class="token plain">/node_modules/lodash.js</span></div></code></span></li></ul><p class="max-w-xl font-sans mb-paragraph ">Simple and intuitive.</p><p class="max-w-xl font-sans mb-paragraph ">The bundling is achieved with Rollup's <a href="https://github.com/kimmobrunfeldt/blog/blob/8e261e0cc61ecf165f2e291afeb6450243cf79ad/rollup.config.js#L58-L75" class="cursor-pointer underline hover:underline ">manual bundle chunking</a>. The <code>/node_modules/</code> directory is not a valid npm-usable directory, but just a regular directory that groups bundles of external dependencies under a familiar name.</p><p class="max-w-xl font-sans mb-paragraph ">Bundling modules in the described way has an awesome side-effect: the network panel can be directly used as to see which dependencies weigh the most. No need for an external bundle analyzer tool.</p><p class="max-w-xl font-sans mb-paragraph ">It would be fair to question whether this actually matters. Production bundles are anyways going to be minified and optimizing software distributables should be a higher priority than developer convenience in the production environment. I'd have to agree. But still, if it's possible to get equal performance with a simpler solution, I'll take it!</p><div class="ChatDiscussion py-10 mx-auto md:relative md:-left-6 max-w-xs flex flex-col"><p class="ChatMessage from-them">Sounds like a neat setup, but why isn't it used more widely?</p></div><p class="max-w-xl font-sans mb-paragraph ">One big reason is that ES6 modules are a fairly new standard: initial browser support landed in 2017. Fortunately, the tools are improving every day. <a href="https://www.snowpack.dev/" class="cursor-pointer underline hover:underline ">Snowpack</a> uses<!-- --> <abbr title="ES6 Modules">ESM</abbr> exclusively and many frontend libraries in npm are shipping only the<!-- --> <abbr title="ES6 Modules">ESM</abbr> bundles for their latest versions.</p><p class="max-w-xl font-sans mb-paragraph ">Another reason may be that the approach used to be a performance killer in HTTP/1.1. However, nowadays doing multiple requests is just fine! Multiplexing in HTTP/2 solved the network bottleneck.</p><p class="max-w-xl font-sans mb-paragraph ">There's one more possible reason that will be covered in <em class="italic">"Bad #5: Chaining requests at page load"</em>.</p><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="mt-32 mb-20 mx-auto text-rust-5" width="55px" height="55px" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M4.1 9.7l-.3.4l.8.6l.3-.4l-.8-.6zm6 .6l.3.4l.8-.6l-.3-.4l-.8.6zM7.5 14A6.5 6.5 0 0 1 1 7.5H0A7.5 7.5 0 0 0 7.5 15v-1zM14 7.5A6.5 6.5 0 0 1 7.5 14v1A7.5 7.5 0 0 0 15 7.5h-1zM7.5 1A6.5 6.5 0 0 1 14 7.5h1A7.5 7.5 0 0 0 7.5 0v1zm0-1A7.5 7.5 0 0 0 0 7.5h1A6.5 6.5 0 0 1 7.5 1V0zM4 6h1V5H4v1zm6 0h1V5h-1v1zm.9 3.7c-1.7-2.267-5.1-2.267-6.8 0l.8.6a3.25 3.25 0 0 1 5.2 0l.8-.6z" fill="currentColor"></path></g></svg><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="bad-1-messy-architectural-boundaries">Bad #1: Messy architectural boundaries</h2><p class="max-w-xl font-sans mb-paragraph ">Gatsby as a framework provides boundaries and a well-defined API. There's a strict border between your application code and the framework code.</p><p class="max-w-xl font-sans mb-paragraph ">The same cannot be said from the DIY approach. As much as I tried to keep the boundaries clear, the blog code knows too much about the supposedly generic site generator and vice-versa.</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="bad-2-onboarding-experience">Bad #2: Onboarding experience</h2><p class="max-w-xl font-sans mb-paragraph ">Let's say you'd have to onboard a new developer into a website project. Would you rather guide them to read Gatsby's documentation or spend days explaining how a custom system works?</p><p class="max-w-xl font-sans mb-paragraph ">I think we both know the answer. There's even a good chance that a new developer would already have experience with Gatsby.</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="bad-3-everything-consumes-time">Bad #3: Everything consumes time</h2><p class="max-w-xl font-sans mb-paragraph ">While writing a <a href="https://github.com/kimmobrunfeldt/blog/blob/main/src/generator/generateRss.ts" class="cursor-pointer underline hover:underline ">script</a> that generates an RSS feed is fairly straightforward, it takes development time. Small tasks add up. A bunch of time is wasted building mundane supporting functionality, instead of the actual website.</p><p class="max-w-xl font-sans mb-paragraph ">In the do-it-yourself approach, you'll gain a deep understanding of each little part of the build tool. It's likely to be useful knowledge even in the following projects, but unfortunately, it doesn't remove the maintenance burden later. The DIY solution has no public issue discussion, let alone contributors opening PRs that help the whole community.</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="bad-4-slow-iteration">Bad #4: Slow iteration</h2><p class="max-w-xl font-sans mb-paragraph ">The downside of simplicity is slowness. Many other build systems leverage <a href="https://www.gatsbyjs.com/docs/debugging-cache-issues/" class="cursor-pointer underline hover:underline ">caching</a>, <a href="https://gulpjs.com/docs/en/getting-started/working-with-files/" class="cursor-pointer underline hover:underline ">in-memory data structures</a>, and <a href="https://webpack.js.org/concepts/" class="cursor-pointer underline hover:underline ">complex abstractions</a> to improve performance. Since the DIY approach opted for simpler abstractions, the cost is sluggish iteration during development.</p><p class="max-w-xl font-sans mb-paragraph ">The file-based build pipeline could be improved in the future, but at the time of writing, it took around 8 seconds from a code change to seeing it in the browser. That's a tad too much.</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="bad-5-chaining-requests-at-page-load">Bad #5: Chaining requests at page load</h2><p class="max-w-xl font-sans mb-paragraph ">While the Rollup<!-- --> <abbr title="ES6 Modules">ESM</abbr> setup is a pleasant development experience, there's a downside. Using imports causes request chaining in the critical render path. The browser needs to do multiple request-response round trips to fetch all relevant files in the module tree. For example:</p><p class="max-w-xl font-sans mb-paragraph ">Browser loads <code>index.html</code> <em class="italic">(1)</em> which refers to a JS module.</p><pre class="py-0"><code class="language-html"><div class="token-line"><span class="token tag punctuation">&lt;</span><span class="token tag">script</span><span class="token tag"> </span><span class="token tag attr-name">type</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">module</span><span class="token tag attr-value punctuation">"</span><span class="token tag"> </span><span class="token tag attr-name">src</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">./hydrate.js</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token script"></span><span class="token tag punctuation">&lt;/</span><span class="token tag">script</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Browser requests <code>hydrate.js</code> <em class="italic">(2)</em> from the server and parses the file. There's a new import inside.</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token keyword">import</span><span class="token plain"> </span><span class="token maybe-class-name">App</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"./app.js"</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Browser requests <code>app.js</code> <em class="italic">(3)</em> from the server. There's yet another import.</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token keyword">import</span><span class="token plain"> </span><span class="token maybe-class-name">PostsPage</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">"./pages/Posts.js"</span><span class="token plain">  </span><span class="token comment">// (4)</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">This continues until we've reached the final leaf module in the import tree.</p><p class="max-w-xl font-sans mb-paragraph ">The example had 4 round-trips between the browser and the server. The process is slow and web performance tools often complain about chaining requests for a reason. I'm not sure what the real fix would be but maybe <a href="https://github.com/WICG/import-maps" class="cursor-pointer underline hover:underline ">import maps</a> will help in the future.</p><p class="max-w-xl font-sans mb-paragraph ">If you know of a solution, I'd be curious to <a href="https://twitter.com/kimmobrunfeldt" class="cursor-pointer underline hover:underline ">hear about it</a>!</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="conclusion">Conclusion</h2><p class="max-w-xl font-sans mb-paragraph ">Let's summarize the arguments above.</p><p class="max-w-xl font-sans mb-paragraph ">Good:</p><ul class="pl-6  space-y-1"><li class="md:pl-2 flex flex-row items-start space-x-4"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="relative flex-shrink-0 top-[4px]" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M3.5 5.5l-.422-.268L3 5.354V5.5h.5zm2.698-4.24l.421.27l-.421-.27zm2.667 1.51l-.448-.223l.448.224zM7.5 5.5l-.447-.224A.5.5 0 0 0 7.5 6v-.5zm7 5l.4.3l.1-.133V10.5h-.5zm-2.4 3.2l.4.3l-.4-.3zM8.282.769L8.539.34L8.282.77zM0 5v10h1V5H0zm3.922.768L6.619 1.53L5.776.992l-2.698 4.24l.844.536zm4.495-3.22L7.053 5.275l.894.448l1.365-2.73l-.895-.447zM7.5 6h5V5h-5v1zM14 7.5v3h1v-3h-1zm.1 2.7l-2.4 3.2l.8.6l2.4-3.2l-.8-.6zM10.5 14h-5v1h5v-1zM4 12.5v-7H3v7h1zM12.5 6A1.5 1.5 0 0 1 14 7.5h1A2.5 2.5 0 0 0 12.5 5v1zm-7 8A1.5 1.5 0 0 1 4 12.5H3A2.5 2.5 0 0 0 5.5 15v-1zM8.024 1.198c.464.278.635.866.393 1.35l.895.446A2.034 2.034 0 0 0 8.539.34l-.515.858zM11.7 13.4a1.5 1.5 0 0 1-1.2.6v1a2.5 2.5 0 0 0 2-1l-.8-.6zM6.62 1.53c.3-.474.924-.62 1.404-.332L8.54.34a2.034 2.034 0 0 0-2.763.652l.843.537z" fill="currentColor"></path></g></svg><span>Technical simplicity</span></li><li class="md:pl-2 flex flex-row items-start space-x-4"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="relative flex-shrink-0 top-[4px]" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M3.5 5.5l-.422-.268L3 5.354V5.5h.5zm2.698-4.24l.421.27l-.421-.27zm2.667 1.51l-.448-.223l.448.224zM7.5 5.5l-.447-.224A.5.5 0 0 0 7.5 6v-.5zm7 5l.4.3l.1-.133V10.5h-.5zm-2.4 3.2l.4.3l-.4-.3zM8.282.769L8.539.34L8.282.77zM0 5v10h1V5H0zm3.922.768L6.619 1.53L5.776.992l-2.698 4.24l.844.536zm4.495-3.22L7.053 5.275l.894.448l1.365-2.73l-.895-.447zM7.5 6h5V5h-5v1zM14 7.5v3h1v-3h-1zm.1 2.7l-2.4 3.2l.8.6l2.4-3.2l-.8-.6zM10.5 14h-5v1h5v-1zM4 12.5v-7H3v7h1zM12.5 6A1.5 1.5 0 0 1 14 7.5h1A2.5 2.5 0 0 0 12.5 5v1zm-7 8A1.5 1.5 0 0 1 4 12.5H3A2.5 2.5 0 0 0 5.5 15v-1zM8.024 1.198c.464.278.635.866.393 1.35l.895.446A2.034 2.034 0 0 0 8.539.34l-.515.858zM11.7 13.4a1.5 1.5 0 0 1-1.2.6v1a2.5 2.5 0 0 0 2-1l-.8-.6zM6.62 1.53c.3-.474.924-.62 1.404-.332L8.54.34a2.034 2.034 0 0 0-2.763.652l.843.537z" fill="currentColor"></path></g></svg><span>No GraphQL for simple workflows</span></li><li class="md:pl-2 flex flex-row items-start space-x-4"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="relative flex-shrink-0 top-[4px]" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M3.5 5.5l-.422-.268L3 5.354V5.5h.5zm2.698-4.24l.421.27l-.421-.27zm2.667 1.51l-.448-.223l.448.224zM7.5 5.5l-.447-.224A.5.5 0 0 0 7.5 6v-.5zm7 5l.4.3l.1-.133V10.5h-.5zm-2.4 3.2l.4.3l-.4-.3zM8.282.769L8.539.34L8.282.77zM0 5v10h1V5H0zm3.922.768L6.619 1.53L5.776.992l-2.698 4.24l.844.536zm4.495-3.22L7.053 5.275l.894.448l1.365-2.73l-.895-.447zM7.5 6h5V5h-5v1zM14 7.5v3h1v-3h-1zm.1 2.7l-2.4 3.2l.8.6l2.4-3.2l-.8-.6zM10.5 14h-5v1h5v-1zM4 12.5v-7H3v7h1zM12.5 6A1.5 1.5 0 0 1 14 7.5h1A2.5 2.5 0 0 0 12.5 5v1zm-7 8A1.5 1.5 0 0 1 4 12.5H3A2.5 2.5 0 0 0 5.5 15v-1zM8.024 1.198c.464.278.635.866.393 1.35l.895.446A2.034 2.034 0 0 0 8.539.34l-.515.858zM11.7 13.4a1.5 1.5 0 0 1-1.2.6v1a2.5 2.5 0 0 0 2-1l-.8-.6zM6.62 1.53c.3-.474.924-.62 1.404-.332L8.54.34a2.034 2.034 0 0 0-2.763.652l.843.537z" fill="currentColor"></path></g></svg><span>Skipping an abstraction layer</span></li><li class="md:pl-2 flex flex-row items-start space-x-4"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="relative flex-shrink-0 top-[4px]" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M3.5 5.5l-.422-.268L3 5.354V5.5h.5zm2.698-4.24l.421.27l-.421-.27zm2.667 1.51l-.448-.223l.448.224zM7.5 5.5l-.447-.224A.5.5 0 0 0 7.5 6v-.5zm7 5l.4.3l.1-.133V10.5h-.5zm-2.4 3.2l.4.3l-.4-.3zM8.282.769L8.539.34L8.282.77zM0 5v10h1V5H0zm3.922.768L6.619 1.53L5.776.992l-2.698 4.24l.844.536zm4.495-3.22L7.053 5.275l.894.448l1.365-2.73l-.895-.447zM7.5 6h5V5h-5v1zM14 7.5v3h1v-3h-1zm.1 2.7l-2.4 3.2l.8.6l2.4-3.2l-.8-.6zM10.5 14h-5v1h5v-1zM4 12.5v-7H3v7h1zM12.5 6A1.5 1.5 0 0 1 14 7.5h1A2.5 2.5 0 0 0 12.5 5v1zm-7 8A1.5 1.5 0 0 1 4 12.5H3A2.5 2.5 0 0 0 5.5 15v-1zM8.024 1.198c.464.278.635.866.393 1.35l.895.446A2.034 2.034 0 0 0 8.539.34l-.515.858zM11.7 13.4a1.5 1.5 0 0 1-1.2.6v1a2.5 2.5 0 0 0 2-1l-.8-.6zM6.62 1.53c.3-.474.924-.62 1.404-.332L8.54.34a2.034 2.034 0 0 0-2.763.652l.843.537z" fill="currentColor"></path></g></svg><span>Rollup bundling</span></li></ul><p class="max-w-xl font-sans mb-paragraph ">Bad:</p><ul class="pl-6  space-y-1"><li class="md:pl-2 flex flex-row items-start space-x-4"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="relative flex-shrink-0 top-[4px]" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M3.5 9.5H3v.146l.078.122L3.5 9.5zm2.698 4.24l.421-.27l-.421.27zm2.667-1.51l-.448.223l.448-.224zM7.5 9.5V9a.5.5 0 0 0-.447.724L7.5 9.5zm7-5h.5v-.167l-.1-.133l-.4.3zm-2.4-3.2l.4-.3l-.4.3zM8.282 14.231l.257.429l-.257-.429zM1 10V0H0v10h1zm2.078-.232l2.698 4.24l.843-.537l-2.697-4.24l-.844.537zm6.234 2.237L7.947 9.276l-.894.448l1.364 2.729l.895-.447zM7.5 10h5V9h-5v1zM15 7.5v-3h-1v3h1zm-.1-3.3L12.5 1l-.8.6l2.4 3.2l.8-.6zM10.5 0h-5v1h5V0zM3 2.5v7h1v-7H3zm9.5 7.5A2.5 2.5 0 0 0 15 7.5h-1A1.5 1.5 0 0 1 12.5 9v1zm-7-10A2.5 2.5 0 0 0 3 2.5h1A1.5 1.5 0 0 1 5.5 1V0zm3.039 14.66a2.034 2.034 0 0 0 .773-2.655l-.895.448c.242.483.07 1.071-.393 1.35l.515.857zM12.5 1a2.5 2.5 0 0 0-2-1v1a1.5 1.5 0 0 1 1.2.6l.8-.6zM5.776 14.008a2.034 2.034 0 0 0 2.763.652l-.515-.858a1.034 1.034 0 0 1-1.405-.331l-.843.537z" fill="currentColor"></path></g></svg><span>Messy architectural boundaries</span></li><li class="md:pl-2 flex flex-row items-start space-x-4"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="relative flex-shrink-0 top-[4px]" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M3.5 9.5H3v.146l.078.122L3.5 9.5zm2.698 4.24l.421-.27l-.421.27zm2.667-1.51l-.448.223l.448-.224zM7.5 9.5V9a.5.5 0 0 0-.447.724L7.5 9.5zm7-5h.5v-.167l-.1-.133l-.4.3zm-2.4-3.2l.4-.3l-.4.3zM8.282 14.231l.257.429l-.257-.429zM1 10V0H0v10h1zm2.078-.232l2.698 4.24l.843-.537l-2.697-4.24l-.844.537zm6.234 2.237L7.947 9.276l-.894.448l1.364 2.729l.895-.447zM7.5 10h5V9h-5v1zM15 7.5v-3h-1v3h1zm-.1-3.3L12.5 1l-.8.6l2.4 3.2l.8-.6zM10.5 0h-5v1h5V0zM3 2.5v7h1v-7H3zm9.5 7.5A2.5 2.5 0 0 0 15 7.5h-1A1.5 1.5 0 0 1 12.5 9v1zm-7-10A2.5 2.5 0 0 0 3 2.5h1A1.5 1.5 0 0 1 5.5 1V0zm3.039 14.66a2.034 2.034 0 0 0 .773-2.655l-.895.448c.242.483.07 1.071-.393 1.35l.515.857zM12.5 1a2.5 2.5 0 0 0-2-1v1a1.5 1.5 0 0 1 1.2.6l.8-.6zM5.776 14.008a2.034 2.034 0 0 0 2.763.652l-.515-.858a1.034 1.034 0 0 1-1.405-.331l-.843.537z" fill="currentColor"></path></g></svg><span>Onboarding experience</span></li><li class="md:pl-2 flex flex-row items-start space-x-4"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="relative flex-shrink-0 top-[4px]" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M3.5 9.5H3v.146l.078.122L3.5 9.5zm2.698 4.24l.421-.27l-.421.27zm2.667-1.51l-.448.223l.448-.224zM7.5 9.5V9a.5.5 0 0 0-.447.724L7.5 9.5zm7-5h.5v-.167l-.1-.133l-.4.3zm-2.4-3.2l.4-.3l-.4.3zM8.282 14.231l.257.429l-.257-.429zM1 10V0H0v10h1zm2.078-.232l2.698 4.24l.843-.537l-2.697-4.24l-.844.537zm6.234 2.237L7.947 9.276l-.894.448l1.364 2.729l.895-.447zM7.5 10h5V9h-5v1zM15 7.5v-3h-1v3h1zm-.1-3.3L12.5 1l-.8.6l2.4 3.2l.8-.6zM10.5 0h-5v1h5V0zM3 2.5v7h1v-7H3zm9.5 7.5A2.5 2.5 0 0 0 15 7.5h-1A1.5 1.5 0 0 1 12.5 9v1zm-7-10A2.5 2.5 0 0 0 3 2.5h1A1.5 1.5 0 0 1 5.5 1V0zm3.039 14.66a2.034 2.034 0 0 0 .773-2.655l-.895.448c.242.483.07 1.071-.393 1.35l.515.857zM12.5 1a2.5 2.5 0 0 0-2-1v1a1.5 1.5 0 0 1 1.2.6l.8-.6zM5.776 14.008a2.034 2.034 0 0 0 2.763.652l-.515-.858a1.034 1.034 0 0 1-1.405-.331l-.843.537z" fill="currentColor"></path></g></svg><span>Everything consumes time</span></li><li class="md:pl-2 flex flex-row items-start space-x-4"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="relative flex-shrink-0 top-[4px]" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M3.5 9.5H3v.146l.078.122L3.5 9.5zm2.698 4.24l.421-.27l-.421.27zm2.667-1.51l-.448.223l.448-.224zM7.5 9.5V9a.5.5 0 0 0-.447.724L7.5 9.5zm7-5h.5v-.167l-.1-.133l-.4.3zm-2.4-3.2l.4-.3l-.4.3zM8.282 14.231l.257.429l-.257-.429zM1 10V0H0v10h1zm2.078-.232l2.698 4.24l.843-.537l-2.697-4.24l-.844.537zm6.234 2.237L7.947 9.276l-.894.448l1.364 2.729l.895-.447zM7.5 10h5V9h-5v1zM15 7.5v-3h-1v3h1zm-.1-3.3L12.5 1l-.8.6l2.4 3.2l.8-.6zM10.5 0h-5v1h5V0zM3 2.5v7h1v-7H3zm9.5 7.5A2.5 2.5 0 0 0 15 7.5h-1A1.5 1.5 0 0 1 12.5 9v1zm-7-10A2.5 2.5 0 0 0 3 2.5h1A1.5 1.5 0 0 1 5.5 1V0zm3.039 14.66a2.034 2.034 0 0 0 .773-2.655l-.895.448c.242.483.07 1.071-.393 1.35l.515.857zM12.5 1a2.5 2.5 0 0 0-2-1v1a1.5 1.5 0 0 1 1.2.6l.8-.6zM5.776 14.008a2.034 2.034 0 0 0 2.763.652l-.515-.858a1.034 1.034 0 0 1-1.405-.331l-.843.537z" fill="currentColor"></path></g></svg><span>Slow iteration during development</span></li><li class="md:pl-2 flex flex-row items-start space-x-4"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" style="transform:rotate(360deg)" class="relative flex-shrink-0 top-[4px]" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 15 15"><g fill="none"><path d="M3.5 9.5H3v.146l.078.122L3.5 9.5zm2.698 4.24l.421-.27l-.421.27zm2.667-1.51l-.448.223l.448-.224zM7.5 9.5V9a.5.5 0 0 0-.447.724L7.5 9.5zm7-5h.5v-.167l-.1-.133l-.4.3zm-2.4-3.2l.4-.3l-.4.3zM8.282 14.231l.257.429l-.257-.429zM1 10V0H0v10h1zm2.078-.232l2.698 4.24l.843-.537l-2.697-4.24l-.844.537zm6.234 2.237L7.947 9.276l-.894.448l1.364 2.729l.895-.447zM7.5 10h5V9h-5v1zM15 7.5v-3h-1v3h1zm-.1-3.3L12.5 1l-.8.6l2.4 3.2l.8-.6zM10.5 0h-5v1h5V0zM3 2.5v7h1v-7H3zm9.5 7.5A2.5 2.5 0 0 0 15 7.5h-1A1.5 1.5 0 0 1 12.5 9v1zm-7-10A2.5 2.5 0 0 0 3 2.5h1A1.5 1.5 0 0 1 5.5 1V0zm3.039 14.66a2.034 2.034 0 0 0 .773-2.655l-.895.448c.242.483.07 1.071-.393 1.35l.515.857zM12.5 1a2.5 2.5 0 0 0-2-1v1a1.5 1.5 0 0 1 1.2.6l.8-.6zM5.776 14.008a2.034 2.034 0 0 0 2.763.652l-.515-.858a1.034 1.034 0 0 1-1.405-.331l-.843.537z" fill="currentColor"></path></g></svg><span>Chaining requests at page load</span></li></ul><p class="max-w-xl font-sans mb-paragraph ">The final site builds ended up being quite similar in size. Gatsby wins in end-user performance by a small margin since it supports frontend routing. The routing allows faster page changes after the initial load.</p><p class="max-w-xl font-sans mb-paragraph ">So, <em class="italic">what were the key takeaways?</em></p><p class="max-w-xl font-sans mb-paragraph ">For me, building a site generator was definitely worth it. I learned about ES6 modules, Gatsby, web performance, and much more. It also supported my goals for the blog and proved to be an enjoyable coding exercise.</p><p class="max-w-xl font-sans mb-paragraph ">For a professional project, I'd definitely recommend adopting more widely used tools like <a href="https://www.gatsbyjs.com/" class="cursor-pointer underline hover:underline ">Gatsby</a> or <a href="https://github.com/react-static/react-static" class="cursor-pointer underline hover:underline ">react-static</a>.</p><p class="max-w-xl font-sans mb-paragraph ">Sometimes the goal itself is to walk the unbeaten path. I'll let you decide if that's the case or not.</p></div></body></html>]]></content:encoded><content:media url="https://kimmo.blog/content-assets/5/cover.png" type="image/jpeg" medium="image" xmlns:media="http://search.yahoo.com/mrss/"/></item><item><title><![CDATA[Advanced TypeScript: The ultimate Tailwind typings]]></title><description><![CDATA[Making the ultimate TypeScript typings for Tailwind class names in a React project.
]]></description><link>https://kimmo.blog/posts/6-advanced-typescript-the-ultimate-tailwind-typings/</link><guid isPermaLink="false">https://kimmo.blog/posts/6-advanced-typescript-the-ultimate-tailwind-typings/</guid><category><![CDATA[typescript]]></category><category><![CDATA[tailwind]]></category><category><![CDATA[types]]></category><category><![CDATA[advanced]]></category><category><![CDATA[react]]></category><pubDate>Sun, 13 Feb 2022 00:00:00 GMT</pubDate><enclosure url="https://kimmo.blog/content-assets/6/cover.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<html><head></head><body><div data-reactroot=""><p class="max-w-xl font-sans mb-paragraph ">This post is about implementing utilities for the ultimate Tailwind-React ergonomics and typings for it.</p><p class="max-w-xl font-sans mb-paragraph ">In the process, we'll learn about most of the advanced TypeScript features including complex conditionals, template literals, and recursive types. The typings are demonstrated via React components, but the final solution would work in any TypeScript project.</p><p class="max-w-xl font-sans mb-paragraph ">To get an idea what we're building, you can check out the final monstrous typings in a Replit project:</p><ul class="pl-6 list-disc space-y-1"><li class="md:pl-2"><a href="https://replit.com/@KimmoBrunfeldt/the-ultimate-tailwind-typings#src/tailwindUtils.ts" class="cursor-pointer underline hover:underline ">Typings and classnames utility</a></li><li class="md:pl-2"><a href="https://replit.com/@KimmoBrunfeldt/the-ultimate-tailwind-typings#src/Examples.tsx" class="cursor-pointer underline hover:underline ">Example usage in components</a></li></ul><p class="max-w-xl font-sans mb-paragraph "><em class="italic">You need to fork the repl if you want to enable TypeScript IntelliSense. Note that the typing hints appear slower in the online environment compared to a local setup.</em></p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="quick-intro-to-tailwind">Quick intro to Tailwind</h2><p class="max-w-xl font-sans mb-paragraph ">Tailwind's website has an accurate pitch that describes its purpose well:</p><blockquote class="text-gray-6 dark:text-gray-5 italic py-2 px-7 my-6 text-center text-lg md:relative md:-left-6"><p class="max-w-xl font-sans mb-paragraph ">A utility-first CSS framework packed with classes like flex, pt-4, text-center and rotate-90 that can be composed to build any design, directly in your markup.</p></blockquote><p class="max-w-xl font-sans mb-paragraph ">If you're sceptical about Tailwind in general, I suggest reading <a href="https://adamwathan.me/css-utility-classes-and-separation-of-concerns/" class="cursor-pointer underline hover:underline ">CSS Utility Classes and "Separation of Concerns"</a> by Adam Wathan. Anyways, Tailwind produces plain CSS, everything is customisable via the config file, and its public API is the class names.</p><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="constraints">Constraints</h3><p class="max-w-xl font-sans mb-paragraph ">There are a few constraints that affect the shape of our solution. The first constraint is about string concatenation. Using Tailwind in dynamic components, such as in React, has <a href="https://tailwindcss.com/docs/optimizing-for-production#writing-purgeable-html" class="cursor-pointer underline hover:underline ">one vital rule</a>.</p><blockquote class="text-gray-6 dark:text-gray-5 italic py-2 px-7 my-6 text-center text-lg md:relative md:-left-6"><p class="max-w-xl font-sans mb-paragraph ">It is important to avoid dynamically creating class strings in your templates with string concatenation, otherwise PurgeCSS won't know to preserve those classes.</p></blockquote><p class="max-w-xl font-sans mb-paragraph ">In other words, don't do this:</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token tag punctuation">&lt;</span><span class="token tag">p</span><span class="token tag"> </span><span class="token tag attr-name">className</span><span class="token tag script language-javascript script-punctuation punctuation">=</span><span class="token tag script language-javascript punctuation">{</span><span class="token tag script language-javascript template-string template-punctuation string">`</span><span class="token tag script language-javascript template-string string">text-</span><span class="token tag script language-javascript template-string interpolation interpolation-punctuation punctuation">${</span><span class="token tag script language-javascript template-string interpolation">color</span><span class="token tag script language-javascript template-string interpolation interpolation-punctuation punctuation">}</span><span class="token tag script language-javascript template-string template-punctuation string">`</span><span class="token tag script language-javascript punctuation">}</span><span class="token tag punctuation">&gt;</span><span class="token maybe-class-name">Hello</span><span class="token operator">!</span><span class="token tag punctuation">&lt;/</span><span class="token tag">p</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">When doing string concatenation, Tailwind doesn't know which specific <code>text-</code> color classes to include in the CSS build. It uses a regex to find complete strings from the source code to detect which Tailwind classes are actually used in the project. Using the information, Tailwind can purge unnecessary CSS.</p><p class="max-w-xl font-sans mb-paragraph ">You <em class="italic">can</em> configure a static list of regex class name patterns that Tailwind will leave to the final build, but it easily leads to bloated CSS.</p><p class="max-w-xl font-sans mb-paragraph ">Purging is important, because there are a huge amount of possible class names. It might be a bit surprising, but there are actually tens of thousands of class names in Tailwind by default. Unpurged <code>output.css</code> from Tailwind can weigh even 8MB <em class="italic">(unminified, uncompressed)</em>.</p><p class="max-w-xl font-sans mb-paragraph ">It sounds scary, but in reality your application will use only a fraction of them. Maybe a few thousands at maximum, which totals to some 10kB.</p><p class="max-w-xl font-sans mb-paragraph "><a href="https://kimmo.blog/content-assets/6/tailwind-classes-venn.svg" rel="noreferrer" target="_blank"><img src="https://kimmo.blog/content-assets/6/tailwind-classes-venn.svg" alt="Tailwind class names Venn diagram" class="my-12"></a></p><p class="max-w-xl font-sans mb-paragraph ">The second constraint is that Tailwind class names have equal specificity. You can't override previously written class names as you might intuitively expect:</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token tag punctuation">&lt;</span><span class="token tag">button</span><span class="token tag"> </span><span class="token tag attr-name">className</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">rounded p-4 flex p-0</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token maybe-class-name">Ok</span><span class="token tag punctuation">&lt;/</span><span class="token tag">button</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">In the above example <code>p-0</code> does not override <code>p-4</code>. Whichever class is defined first in Tailwind's <code>output.css</code> wins. That's how CSS works in general. Another example for a real use case:</p><pre class="py-0"><code class="language-tsx"><div class="token-line"><span class="token comment">// Doesn't work. Background color and hover effect</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// can't be changed by appending new classes to the end.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> baseCls </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string">"rounded p-4 bg-primary hover:cursor-pointer"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> disabledCls </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string">"bg-gray-200 hover:cursor-not-allowed"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> finalCls </span><span class="token operator">=</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string string"></span></div><div class="token-line"><span class="token template-string string">  </span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">baseCls</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string string"></span></div><div class="token-line"><span class="token template-string string">  </span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">props</span><span class="token template-string interpolation punctuation">.</span><span class="token template-string interpolation">disabled </span><span class="token template-string interpolation operator">?</span><span class="token template-string interpolation"> disabledCls </span><span class="token template-string interpolation operator">:</span><span class="token template-string interpolation"> </span><span class="token template-string interpolation string">''</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string string"></span></div><div class="token-line"><span class="token template-string string"></span><span class="token template-string template-punctuation string">`</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// Do this instead.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> baseCls </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string">"rounded p-4"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> primaryCls </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string">"bg-primary hover:cursor-pointer"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> disabledCls </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string">"bg-gray-200 hover:cursor-not-allowed"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> finalCls </span><span class="token operator">=</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string string"></span></div><div class="token-line"><span class="token template-string string">  </span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">baseCls</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string string"></span></div><div class="token-line"><span class="token template-string string">  </span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">props</span><span class="token template-string interpolation punctuation">.</span><span class="token template-string interpolation">disabled</span></div><div class="token-line"><span class="token template-string interpolation">    </span><span class="token template-string interpolation operator">?</span><span class="token template-string interpolation"> disabledCls</span></div><div class="token-line"><span class="token template-string interpolation">    </span><span class="token template-string interpolation operator">:</span><span class="token template-string interpolation"> primaryCls</span></div><div class="token-line"><span class="token template-string interpolation">   </span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string string"></span></div><div class="token-line"><span class="token template-string string"></span><span class="token template-string template-punctuation string">`</span><span class="token punctuation">;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Here's a <a href="https://github.com/tailwindlabs/tailwindcss/issues/1010" class="cursor-pointer underline hover:underline ">GitHub issue discussion</a> about this commonly appearing topic.</p><p class="max-w-xl font-sans mb-paragraph ">Now that we know these limitations, the following decisions make more sense.</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="the-desired-result">The desired result</h2><p class="max-w-xl font-sans mb-paragraph ">It sounds like a trivial task: simply add Tailwind class names to <code>className</code> prop. Well, it actually is if you are happy with the ergonomics, but we are aiming pretty high here <em class="italic">(read: over engineering)</em>. Achieving a great developer experience with a clean, readable, and type-safe solution with all the above constraints in mind does require some thinking.</p><p class="max-w-xl font-sans mb-paragraph ">There are multiple ways to dynamically format <code>className</code> strings, but to me <a href="https://www.npmjs.com/package/classnames" class="cursor-pointer underline hover:underline ">classnames</a> provides the perfect balance of readability and convenience. It's great as is, but there's nothing guarding you from typos or using Tailwind class names that were purged for some reason.</p><p class="max-w-xl font-sans mb-paragraph ">What we want is <em class="italic">classnames</em>, but with strict typings. Let's call this new more strict function <code>cn</code>.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">import</span><span class="token plain"> </span><span class="token imports">classnames</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">'classnames'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// TODO: Implement typings</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> </span><span class="token function-variable function">cn</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token spread operator">...</span><span class="token plain">args</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">Todo</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token arrow operator">=&gt;</span><span class="token plain"> </span><span class="token function">classnames</span><span class="token punctuation">(</span><span class="token spread operator">...</span><span class="token plain">args</span><span class="token punctuation">)</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">After implementing the types, we should be able to catch all the incorrect calls at type-level. The API of <em class="italic">classnames</em> is flexible, let's look at the different possibilities we need to be able to handle.</p><p class="max-w-xl font-sans mb-paragraph ">Regular class name string.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">const</span><span class="token plain"> </span><span class="token function-variable function maybe-class-name">Hello</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token arrow operator">=&gt;</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token comment">// typo in regular string</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">return</span><span class="token plain"> </span><span class="token operator">&lt;</span><span class="token plain">p className</span><span class="token operator">=</span><span class="token punctuation">{</span><span class="token function">cn</span><span class="token punctuation">(</span><span class="token template-string template-punctuation string">`</span><span class="token template-string string">text-grau-100 bg-white</span><span class="token template-string template-punctuation string">`</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token maybe-class-name">Hello</span><span class="token operator">!</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">&lt;</span><span class="token operator">/</span><span class="token plain">p</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">An array of class name strings.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Props</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> disabled</span><span class="token operator">?</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">boolean</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> </span><span class="token function-variable function maybe-class-name">Hello2</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token plain"> disabled </span><span class="token punctuation">}</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">Props</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token arrow operator">=&gt;</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">return</span><span class="token plain"> </span><span class="token operator">&lt;</span><span class="token plain">p className</span><span class="token operator">=</span><span class="token punctuation">{</span><span class="token function">cn</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token string">'flex my-5'</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// typo in a string in the array format</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token template-string template-punctuation string">`</span><span class="token template-string string">text-grau-800 bg-white</span><span class="token template-string template-punctuation string">`</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    disabled </span><span class="token operator">?</span><span class="token plain"> </span><span class="token string">'bg-gray-300 cursor-not-allowed'</span><span class="token plain"> </span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">''</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token maybe-class-name">Hello</span><span class="token operator">!</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">&lt;</span><span class="token operator">/</span><span class="token plain">p</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">An object where each key is a string of class names.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Props</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> dense</span><span class="token operator">?</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">boolean</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> </span><span class="token function-variable function maybe-class-name">Hello3</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token plain"> dense </span><span class="token operator">=</span><span class="token plain"> </span><span class="token boolean">false</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token arrow operator">=&gt;</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">return</span><span class="token plain"> </span><span class="token operator">&lt;</span><span class="token plain">p className</span><span class="token operator">=</span><span class="token punctuation">{</span><span class="token function">cn</span><span class="token punctuation">(</span><span class="token template-string template-punctuation string">`</span><span class="token template-string string">text-gray-800 bg-white</span><span class="token template-string template-punctuation string">`</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// typo in object key format</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token string">'px-asd2 py-1 my-2'</span><span class="token operator">:</span><span class="token plain"> dense</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// try this:</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// 'px-2 py-1 my-2': dense,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token string">'px-3 py-2 my-3'</span><span class="token operator">:</span><span class="token plain"> </span><span class="token operator">!</span><span class="token plain">dense</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token maybe-class-name">Hello</span><span class="token operator">!</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">&lt;</span><span class="token operator">/</span><span class="token plain">p</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">A combination of multi-line class name string and object format.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Props</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> dense</span><span class="token operator">?</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">boolean</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> </span><span class="token function-variable function maybe-class-name">Hello4</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token plain"> dense </span><span class="token operator">=</span><span class="token plain"> </span><span class="token boolean">false</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">Props</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token arrow operator">=&gt;</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">return</span><span class="token plain"> </span><span class="token operator">&lt;</span><span class="token plain">p</span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// long class name definition with a typo.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// note the leading and trailing spaces</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// on each line</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    className</span><span class="token operator">=</span><span class="token punctuation">{</span><span class="token function">cn</span><span class="token punctuation">(</span><span class="token template-string template-punctuation string">`</span><span class="token template-string string"></span></div><div class="token-line"><span class="token template-string string">      flexxx</span></div><div class="token-line"><span class="token template-string string">      text-gray-800 bg-white</span></div><div class="token-line"><span class="token template-string string">      hover:text-gray-900</span></div><div class="token-line"><span class="token template-string string">      hover:border-b-2</span></div><div class="token-line"><span class="token template-string string">    </span><span class="token template-string template-punctuation string">`</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token string">'px-1 py-3 my-4'</span><span class="token operator">:</span><span class="token plain"> dense</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token string">'px-2 py-4 my-6'</span><span class="token operator">:</span><span class="token plain"> </span><span class="token operator">!</span><span class="token plain">dense</span></div><div class="token-line"><span class="token plain">    </span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token maybe-class-name">Hello</span><span class="token operator">!</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">&lt;</span><span class="token operator">/</span><span class="token plain">p</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">If we were able to raise type errors for all the cases, that'd be the holy grail. Sleeves up!</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="implementation">Implementation</h2><p class="max-w-xl font-sans mb-paragraph ">It might look just moderately complex to type the <em class="italic">classnames</em> API, but it gets tough. Chewing the complexity into smaller pieces helps. The individual pieces we need for the typings are:</p><ol class="pl-6 list-decimal space-y-1"><li class="md:pl-2">Type containing all valid Tailwind class names. For example <code>type ClassName = 'p-1' | 'p-2' | 'etc'</code>.</li><li class="md:pl-2">Type that trims leading and trailing whitespace of strings.</li><li class="md:pl-2">Error message type that shows which is the offending token. For example <code>'flexxx' is not a valid Tailwind class</code>.</li><li class="md:pl-2">A way to split the whole <code>className</code> string into word tokens and validate them against <code>ClassName</code>.</li><li class="md:pl-2">Support for a class name string</li><li class="md:pl-2">Support for the object format</li><li class="md:pl-2">Support for the array format</li></ol><p class="max-w-xl font-sans mb-paragraph ">Let's cover them one by one, and tie it all together in the end.</p><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="1-tailwind-class-names">1. Tailwind class names</h3><p class="max-w-xl font-sans mb-paragraph ">There's a few different routes for generating a union type out of all class names. One solution would be to use the ability to expand template literal type unions:</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">ColorName</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string">'red'</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'purple'</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'blue'</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'green'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Luminance</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token number">100</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token number">200</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token number">300</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token number">400</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token number">500</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token number">600</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token number">700</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token number">800</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token number">900</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Color</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">ColorName</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string string">-</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">Luminance</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Layout</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string">'block'</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'flex'</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'grid'</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'inline-block'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// Expands to all possible class names</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">ClassName</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">Layout</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string string">text-</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">Color</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string string">bg-</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">Color</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">But it's a lot of work and the typings would need to be generated based on <a href="https://tailwindcss.com/docs/configuration" class="cursor-pointer underline hover:underline ">tailwind.config.js</a>. In addition, it would lead to a huge union type with more than 60 000 unique string literals.</p><p class="max-w-xl font-sans mb-paragraph ">Instead of template literal expansion, I found that <a href="https://github.com/esamattis/postcss-ts-classnames" class="cursor-pointer underline hover:underline ">postcss-ts-classnames</a> works well. It parses the output of PostCSS, exctracts all class names, and writes the union type of them to a <code>d.ts</code> file.</p><p class="max-w-xl font-sans mb-paragraph ">The result is a robust typing, that doesn't care about Tailwind's configuration or purging. It accurately captures all the class names your project has.</p><p class="max-w-xl font-sans mb-paragraph ">A downside is that we introduce a small lag to the development cycle:</p><ul class="pl-6 list-disc space-y-1"><li class="md:pl-2">A new valid Tailwind class name is taken into use</li><li class="md:pl-2">For a short while, IDE complains about the class not being valid</li><li class="md:pl-2">PostCSS build is ran, which re-generates the <code>d.ts</code> types</li><li class="md:pl-2">IDE does a TypeScript refresh, and the error is gone</li></ul><p class="max-w-xl font-sans mb-paragraph ">That said, the lag isn't a big issue in reality. The union type performs well enough even if your project happens to use a few thousand unique class names.</p><p class="max-w-xl font-sans mb-paragraph ">Now we have <code>tailwindClassNames.d.ts</code> which contains a union type of all valid class names:</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">ClassName</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'block'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'inline-block'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'flex'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'grid'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'bg-gray-100'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'p-1'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token comment">// ... a thousand more</span><span class="token plain"></span></div></code></pre><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="2-trimming-whitespace">2. Trimming whitespace</h3><p class="max-w-xl font-sans mb-paragraph ">For this trick, we'll use <a href="https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html#inference-with-template-literals" class="cursor-pointer underline hover:underline ">inference with template literals</a>, <a href="https://www.typescriptlang.org/docs/handbook/2/conditional-types.html" class="cursor-pointer underline hover:underline ">conditional types</a>, and type recursion. To get started, here's the generously commented <code>TrimStart</code> utility that deals with the leading whitespace.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token comment">// Newlines occur in multiline strings with backtick quotes</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Whitespace</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string">' '</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'\n'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// `extends string` validates that the</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// type argument `T` is a string</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">TrimStart</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name"> </span><span class="token class-name keyword">extends</span><span class="token class-name"> </span><span class="token class-name builtin">string</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token comment">// Is `T` in format: ` ${Tail}` or `\n${Tail}`?</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token constant">T</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">Whitespace</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">infer Tail</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// Yes. Recursively call this utility again,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// because Tail might contain new leading</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// whitespace.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">?</span><span class="token plain"> </span><span class="token maybe-class-name">TrimStart</span><span class="token operator">&lt;</span><span class="token maybe-class-name">Tail</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// No. Stop recursion.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">:</span><span class="token plain"> </span><span class="token constant">T</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph "><code>TrimStart</code> loops recursively until it has "eaten" all the whitespace characters from the beginning. You could think it as a while-loop, but done with recursion.</p><pre class="py-0"><code class="language-javascript"><div class="token-line"><span class="token keyword control-flow">while</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token plain">str</span><span class="token punctuation">.</span><span class="token method function property-access">startsWith</span><span class="token punctuation">(</span><span class="token string">' '</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token operator">||</span><span class="token plain"> str</span><span class="token punctuation">.</span><span class="token method function property-access">startsWith</span><span class="token punctuation">(</span><span class="token string">'\n'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  str </span><span class="token operator">=</span><span class="token plain"> str</span><span class="token punctuation">.</span><span class="token method function property-access">substring</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">In each iteration of the recursive loop, only one whitespace character is consumed at a time. This happens because in the conditional, the type argument <code>T</code> needs to exactly match the template <code>`${Whitespace}${infer Tail}`</code>. Since <code>Whitespace</code> can only be 1 character long (' ' or '\n'), it consumes a single whitespace character at a time.</p><p class="max-w-xl font-sans mb-paragraph ">Here's a few example results (<a href="https://www.typescriptlang.org/play?#code/PTAEDkFMHcBsEsB2kDOoD2BjTBXATqEqALY6wAu8CyoK5eSA5mtPOQBagBGAhpgNaUBoAI4505VAFgAUOQCeAB0igA6uzapFfFQF5QAckOgAPoYA6iA7NkhQAA0gAPSYgAmaOg0SN7oAG48CG48kmgcoaAckLZgCsqgPHiMOMSQiOQOACp+8Gg8tPRMsvEqWQzEAMrkSeQAPFmgzq4ehd6MAHygurKgoHYAkmj2OYSIoABm6HjEoQBcDqAAJADeWTxUAL5+0w6Wq+tb9gD8vaCNzemt9qvqmijamJCbq0gTkASHsNtnfXYAmqgAHSgABKkFweBQ8H8kFg8lAmCCsCiGjQOEoCAUiUYG0QABpfv0wFwITwcCgyhsUcR4Ix2JlMOgMnjQMhoKBYJAeG5ijI+n8wNANGFHpAgUTjucKtVag1qR0iXZwOgQdV0IpQHgIfhocyJfyBQssjYZCUlCpwSgyOQAIzdaXwKo1PD1IygSygACCBkVcgtYNQNod5SdstddXd3r6vqAA" class="cursor-pointer underline hover:underline ">TypeScript playground</a>).</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token comment">// type Result1 = "A"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Result1</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">TrimStart</span><span class="token operator">&lt;</span><span class="token string">'  \n A'</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// type Result2 = "A  "</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Result2</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">TrimStart</span><span class="token operator">&lt;</span><span class="token string">'   A  '</span><span class="token operator">&gt;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">It's a neat trick, but there is a maximum type instantiation depth limit in TypeScript which limits the recursion depth. It is possible to hit the limit when trying to trim a string with very long leading whitespace.</p><p class="max-w-xl font-sans mb-paragraph ">Fortunately, there's an optimization trick. You can pre-define a few different lengths of whitespaces to minimize the amount of recursion depth required:</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">InstantiationDepthReducingWhitespace</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token comment">// This will match first, consuming 3 leading spaces at once</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'   '</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'  '</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">' '</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'\n'</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Clever! Now let's implement <code>TrimEnd</code> and combine them together:</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">InstantiationDepthReducingWhitespace</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'   '</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'  '</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">' '</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string">'\n'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">TrimStart</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name"> </span><span class="token class-name keyword">extends</span><span class="token class-name"> </span><span class="token class-name builtin">string</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token constant">T</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">InstantiationDepthReducingWhitespace</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">infer Tail</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">?</span><span class="token plain"> </span><span class="token maybe-class-name">TrimStart</span><span class="token operator">&lt;</span><span class="token maybe-class-name">Tail</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">:</span><span class="token plain"> </span><span class="token constant">T</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">TrimEnd</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name"> </span><span class="token class-name keyword">extends</span><span class="token class-name"> </span><span class="token class-name builtin">string</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token constant">T</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">infer Prefix</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">InstantiationDepthReducingWhitespace</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">?</span><span class="token plain"> </span><span class="token maybe-class-name">TrimEnd</span><span class="token operator">&lt;</span><span class="token maybe-class-name">Prefix</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">:</span><span class="token plain"> </span><span class="token constant">T</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Trim</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name"> </span><span class="token class-name keyword">extends</span><span class="token class-name"> </span><span class="token class-name builtin">string</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">TrimEnd</span><span class="token operator">&lt;</span><span class="token maybe-class-name">TrimStart</span><span class="token operator">&lt;</span><span class="token constant">T</span><span class="token operator">&gt;&gt;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">The end result:</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token comment">// type Result = "abc"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Result</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">Trim</span><span class="token operator">&lt;</span><span class="token string">'\n\n  abc   '</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// The need for trimming long whitespace appears</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// in deeply nested (more indentation) multi-line strings:</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">function</span><span class="token plain"> </span><span class="token function">doSomething</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">if</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token plain">condition</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token keyword">if</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token plain">another</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token function">cn</span><span class="token punctuation">(</span><span class="token template-string template-punctuation string">`</span><span class="token template-string string"></span></div><div class="token-line"><span class="token template-string string">        block</span></div><div class="token-line"><span class="token template-string string">        p-1</span></div><div class="token-line"><span class="token template-string string">        hover:bg-blue</span></div><div class="token-line"><span class="token template-string string">      </span><span class="token template-string template-punctuation string">`</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph "><strong class="font-bold">For the curious minds:</strong> the type instantiation depth was first <a href="https://github.com/microsoft/TypeScript/pull/45025" class="cursor-pointer underline hover:underline ">increased to 500</a> but later <a href="https://github.com/microsoft/TypeScript/pull/45711" class="cursor-pointer underline hover:underline ">reduced back to a 100</a>, which isn't a huge amount for template literal processing.</p><p class="max-w-xl font-sans mb-paragraph ">The same <a href="https://github.com/microsoft/TypeScript/pull/45711" class="cursor-pointer underline hover:underline ">pull request</a>, which lowered the type instantiation depth limit to 100, also implemented tail recursive evaluation of conditional types. The description has examples of how the <code>Trim</code> utility could be even further improved.</p><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="3-error-messages">3. Error messages</h3><p class="max-w-xl font-sans mb-paragraph ">The built-in way to indicate a type error is to use <a href="https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-never-type" class="cursor-pointer underline hover:underline ">never</a>. It works because <code>never</code> is not assignable to any other type.</p><p class="max-w-xl font-sans mb-paragraph ">In complex conditional types you might have multiple instances of <code>never</code> type, but for separate issues. This makes the code hard to read and type errors less descriptive. Unfortunately, TypeScript <a href="https://github.com/microsoft/TypeScript/issues/23689" class="cursor-pointer underline hover:underline ">doesn't natively support</a> anything like <code>throw</code>.</p><p class="max-w-xl font-sans mb-paragraph ">However, there is a hack that achieves almost the same result. Instead of returning <code>never</code>, you can return the error message as a literal string:</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">IsValid</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name"> </span><span class="token class-name keyword">extends</span><span class="token class-name"> </span><span class="token class-name builtin">string</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token constant">T</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token class-name maybe-class-name">ClassName</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">?</span><span class="token plain"> </span><span class="token constant">T</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">:</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string string">Error: '</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation constant">T</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string string">' is not a valid Tailwind class</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">The literal string error message works in a similar way as <code>never</code>. Types aren't assignable to it in practice. It's easiest to understand via an example (<a href="https://www.typescriptlang.org/play?#code/C4TwDgpgBAwgNgQwM5IHIILbQLwCgpQA+UA5AEZwD2AxgNYn5EAFJAlgHZwcQC0FN9RsRIAzOBAAeDAsIDmAJ1YATaUVJlZPBQhA8AjAAYDqgPQmoAOitQEUYAAtKAVyQJ2SqBkryIuP6EgoAEkkADUELiUAHgAVKElgCHckKCRgRXZZAD4oPAI4hKSlFPhkNExfAgIAfigYxgIALigAAwBReXlvZpIAEgBvGIBfEihWFPZKYBsoADcI5TqEVjgAdw4PakQUFr9cESd2amBWSnYoanZY+IlE5NT0jmyACiQn8VKUdCxmkPDI2JZACUUH6jB8wCc8nOb0yH225SwAG5cEM-GYoABBeSyJxYdjTSgiOzgaAkABEHHmkXJo3GUEm0zKrFk7AQFGgwEoUDACHkFUS8igRJJkFwGIpHS68h6VIWKjGEymM2pixiyzWGwuCNpFlwl2ebHYqpUQKAA" class="cursor-pointer underline hover:underline ">TypeScript playground</a>):</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">function</span><span class="token plain"> </span><span class="token generic-function function">cn</span><span class="token generic-function generic class-name operator">&lt;</span><span class="token generic-function generic class-name constant">T</span><span class="token generic-function generic class-name"> </span><span class="token generic-function generic class-name keyword">extends</span><span class="token generic-function generic class-name"> </span><span class="token generic-function generic class-name builtin">string</span><span class="token generic-function generic class-name operator">&gt;</span><span class="token punctuation">(</span><span class="token plain">singleClassName</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">IsValid</span><span class="token operator">&lt;</span><span class="token constant">T</span><span class="token operator">&gt;</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">return</span><span class="token plain"> singleClassName</span><span class="token punctuation">;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// Argument of type '"invalid"' is not assignable to parameter of type</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// '"Error: 'invalid' is not a valid Tailwind class"'.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token function">cn</span><span class="token punctuation">(</span><span class="token string">'invalid'</span><span class="token punctuation">)</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">The passed parameter typed as literal <code>'invalid'</code> is not assignable to the error message that is also a literal string type. This pattern can be extracted as a utility type to make the intention more clear:</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token comment">// Utility type to provide nicer error messages</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Err</span><span class="token class-name operator">&lt;</span><span class="token class-name maybe-class-name">Message</span><span class="token class-name"> </span><span class="token class-name keyword">extends</span><span class="token class-name"> </span><span class="token class-name builtin">string</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string string">Error: </span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">Message</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div></code></pre><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="4-split-to-tokens-and-validate">4. Split to tokens and validate</h3><p class="max-w-xl font-sans mb-paragraph ">This is going to be a tough piece.</p><p class="max-w-xl font-sans mb-paragraph ">To ease the token processing, let's make a utility that converts <code>className</code> string into an array of tokens. Working with an array is easier than for example a string with space separated values.</p><p class="max-w-xl font-sans mb-paragraph ">The new utility called <code>SplitToTailwindClassNames</code> should do the following conversions:</p><table><thead><tr><th>Input</th><th>Output</th></tr></thead><tbody><tr><td><code>"block bg-gray-100"</code></td><td><code>["block", "bg-gray-100"]</code></td></tr><tr><td><code>"\n\n block\n  p-1  "</code></td><td><code>["block", "p-1"]</code></td></tr><tr><td><code>" block invalid "</code></td><td><code>["block", never]</code></td></tr></tbody></table><p class="max-w-xl font-sans mb-paragraph ">We'll start with the familiar template literal inference. This time instead of consuming whitespace characters, we want to consume Tailwind class names.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">SplitToTailwindClassNames</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name"> </span><span class="token class-name keyword">extends</span><span class="token class-name"> </span><span class="token class-name builtin">string</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token comment">// Does `T` start with a Tailwind class name?</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token constant">T</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">ClassName</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">infer Tail</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// Yes. Continue consuming ClassName tokens with</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// next recursion iteration.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">?</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token maybe-class-name">ClassName</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token spread operator">...</span><span class="token maybe-class-name">SplitToTailwindClassNames</span><span class="token operator">&lt;</span><span class="token maybe-class-name">Tail</span><span class="token operator">&gt;</span><span class="token punctuation">]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// No. Stop recursion.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">We can see that the utility now knows how to consume Tailwind class name tokens (<a href="https://www.typescriptlang.org/play?#code/C4TwDgpgBAwgNgQwM5IHIILbQLwCgpQA+UA5AEZwD2AxgNYn5EAFJAlgHZwcQC0FN9RsRIAzOBAAeDAsIDmAJ1YATaUVJlZPBQhA8AjAAYDq4WH2qA9BagA6O1ARRgAC0oBXJAnZKoGSvIhcXFBIKABlMC5gABVKaIRWOAB3DiV4ZDRMCCQAHmioSWAIbyQoJGBFdlkAPig8AisoABFKbKgAA2j2suAEeWAoFJcHKHjElO8oakQUKHYsgH5GfMLipVL2gBIAb3SUdCwAXx2OEQh5UYS4Q-bGBusATWybWEp2YA43aGo3pDcMDiyWAzTJYJyUWjFUpDZx3KCNdiFKABahueRIVhvKCsIryBAfN42OELKAAbT2oIgABpbHYIlFYmNkqkKQdsnkrtUALpwxqoSgvMLAShgZEQVHozHsIkEAgALjJPNwjRC0CaEDIblkejqiuC4DVGq1Ouw4UiOMZVwmaRBbNyJFI1SCKoNzSNsgATLrybasjzVW7NZ7dfSLXErSzfVh7fw6CQncrrAH1UGAMze1lZGmZrD+10prXp02hmLh8aRjJ2nLkKh0Mx6ePO6z8ooKpLOHHZMAIajQVildiUAY-eQo4BwEBQZxeJTiHwgCDAACETac+fdABYM1GIHnQgXZFvi+bS0zrTn2TWBFB6-GgA" class="cursor-pointer underline hover:underline ">TypeScript playground</a>).</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token comment">// type Debug1 = []</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Debug1</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">SplitToTailwindClassNames</span><span class="token operator">&lt;</span><span class="token string">' '</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// type Debug2 = [ClassName]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Debug2</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">SplitToTailwindClassNames</span><span class="token operator">&lt;</span><span class="token string">'block'</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// type Debug3 = [ClassName, ClassName]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Debug3</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">SplitToTailwindClassNames</span><span class="token operator">&lt;</span><span class="token string">'blockp-1'</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// Note: whitespace is not correctly handled yet!</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// type Debug4 = [ClassName]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Debug4</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">SplitToTailwindClassNames</span><span class="token operator">&lt;</span><span class="token string">'block p-1'</span><span class="token operator">&gt;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">That's great but the array items are of type <code>ClassName</code>, which is the union type itself. What we really need is the actual tokens found, for example <code>['block', 'p-1']</code>.</p><p class="max-w-xl font-sans mb-paragraph ">There's no way in TypeScript to make a template literal match a token <strong class="font-bold">and</strong> assign it to a type variable at the same time. If TypeScript supported the feature, it could look something like this:</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token constant">T</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">infer </span><span class="token template-string interpolation constant">C</span><span class="token template-string interpolation"> </span><span class="token template-string interpolation keyword">extends</span><span class="token template-string interpolation"> </span><span class="token template-string interpolation class-name">ClassName</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">infer Tail</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// `C` would refer to the actual token</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">But it doesn't.</p><p class="max-w-xl font-sans mb-paragraph ">I couldn't find a solution anywhere online, which lead me to <a href="https://stackoverflow.com/questions/65844206/how-to-avoid-ambiquity-in-typescript-template-literal-type-inference/65846728" class="cursor-pointer underline hover:underline ">ask the question</a> in Stack Overflow. It took a while to marinate, but finally a solution clicked in my head.</p><p class="max-w-xl font-sans mb-paragraph ">To get access to the first token, you can inverse the inference with a second template literal inference:</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">GetTokenTrick</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name"> </span><span class="token class-name keyword">extends</span><span class="token class-name"> </span><span class="token class-name builtin">string</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token constant">T</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">ClassName</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">infer Tail</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">?</span><span class="token plain"> </span><span class="token constant">T</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">infer </span><span class="token template-string interpolation constant">C</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">Tail</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token operator">?</span><span class="token plain"> </span><span class="token constant">C</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token comment">// This shouldn't be possible to reach, as</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token comment">// we just inversed the matching</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">never</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">never</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Now the utility captures the first token (<a href="https://www.typescriptlang.org/play?#code/C4TwDgpgBAwgNgQwM5IHIILbQLwCgpQA+UA5AEZwD2AxgNYn5EAFJAlgHZwcQC0FN9RsRIAzOBAAeDAsIDmAJ1YATaUVJlZPBQhA8AjAAYDq4WH2qA9BagA6O1ARRgAC0oBXJAnZKoGSvIhcXFBIKABxCGAAFUpaCHYoxToAHiioSWB4pSQoJGBFdlkAPig8AjSMrJyAAwASAG94ZDRMCABfBo4RCHkoKIRWODbqxgIAfj70iUzvGs72bt6YDvr+weHRgigJmE2CKz7nVhykVzc4JXYSYCgyaDBKFFYKaGBKKACEamcAGgckPZQA4Ad2gACsPDcOAA3HpICA+FzQDAIYDfDiyQEALig7AgsPkmxxeIJQQOIWgABEIGQ3LJSlAAET8OiM4LgKk0ukMiLRWLxRKsFLkKgpYHOVH4npFEhFIA" class="cursor-pointer underline hover:underline ">TypeScript playground</a>)!</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token comment">// type Debug = "block"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Debug</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">GetTokenTrick</span><span class="token operator">&lt;</span><span class="token string">'block&lt;whatever&gt;'</span><span class="token operator">&gt;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Let's combine the knowledge so far and also deal with the whitespace by using <code>Trim&lt;Tail&gt;</code>.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">SplitToTailwindClassNames</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name"> </span><span class="token class-name keyword">extends</span><span class="token class-name"> </span><span class="token class-name builtin">string</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token comment">// Does `T` start with a Tailwind class name?</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token constant">T</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">ClassName</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">infer Tail</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">?</span><span class="token plain"> </span><span class="token constant">T</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">infer </span><span class="token template-string interpolation constant">C</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">Tail</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token comment">// Continue consuming ClassName tokens with</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token comment">// next recursion iteration.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token operator">?</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token constant">C</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token spread operator">...</span><span class="token maybe-class-name">SplitToTailwindClassNames</span><span class="token operator">&lt;</span><span class="token maybe-class-name">Trim</span><span class="token operator">&lt;</span><span class="token maybe-class-name">Tail</span><span class="token operator">&gt;&gt;</span><span class="token punctuation">]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">      </span><span class="token comment">// This shouldn't be possible to reach, as</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token comment">// we just inversed the matching</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">never</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// Stop recursion.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Many cases are now dealt correctly, but there's still a few issues (<a href="https://www.typescriptlang.org/play?#code/C4TwDgpgBAwgNgQwM5IHIILbQLwCgpQA+UA5AEZwD2AxgNYn5EAFJAlgHZwcQC0FN9RsRIAzOBAAeDAsIDmAJ1YATaUVJlZPBQhA8AjAAYDq4WH2qA9BagA6O1ARRgAC0oBXJAnZKoGSvIhcXFBIKABJdiRgL2BWBFjKdgARCDAXACUIJTdqDlkAdWdWYAgkMARqHCFSAlJqkgITGqaSAB12BmDwaAAVRQwAZWj5YAAeHqhJEu8kKCjFdlkAPig8AgmpiBmoAAMAEgBvCKiYuITk1IysnLzC4tLyyoBfQ44RCHkoHoRWOCedxgEAD8X36QwQI3GPzgS0BUAAXF8giFev0AKLecaTCTTJSzeZ5FZrL7Y3GzfYHN4fKAABQCIlYEheR0i0XYsXirESKTSzky2VyizuJTKFQg-zhIL6rAwGKUozpEAZElhtQRSK6oWlGCxm22BMWRNBMrl4zBwzGPSWsM10AGYC4wB6lG+vwA7hwlPBkGhMKVdTitni5sAFstVowrFAkpRSrsejsQxDgFAPS4HF9oR7vFBqIgUFB2H6gYwNoHthTvSh0FhmVTPq6-gC1VLSUHya92O9PjBmY2JWqCFGYIlYuw3NBqIkkG4MHlYPnfVgnJRaFtZmnnHCh9Z2FMoAFqG55EguewoPd5JzEjZt1AQQBtGAAGlsdntjudjezXsXNf92pQr81oALpBIOUBRj0RT4q4bhwEoHQpmQ0BgJQKCsBQ0DAJQB4QBUzivsgd5Rm60AAFYeCmHAAG4fEgWROM40AYPE1BFIsd6InudHyOBtRRkMlBgHhR4nmet5qoiD5gbgUYotGEBkG4sh6KsUAPgARPwdCaWBCkpMpqnqR+xRflmnpVku-rkFQdAkLC8ndIpRkAEzqVpOm0Jpr6aWYeh6baLkqe52BQKZTouhZ3hWf+SCjLZAhQP5DlydYBlKSpADMHnaXZ3m+f5gUZUZOVhRF5nupZf5+vFiV0O07T+bUqVpeFsRwHAhaUCmbr+LQeTwkEUbCg8YoOCmLjQChsgcOweRBYZKkACwmQ6ZlRVVMU1VgdVQF5Dk7hpslRmi8jyP4swQtA7A9VASj4XAKZTudEDUMAcAgM+bVIHBCH7dAD48R8+nOUtsgAKxrZ+m1wD+sW1QltEIFwKgrJB1gyUAA" class="cursor-pointer underline hover:underline ">TypeScript playground</a>).</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token comment">// type Debug1 = ["block"]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Debug1</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">SplitToTailwindClassNames</span><span class="token operator">&lt;</span><span class="token string">'block'</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// type Debug2 = ["block", "p-1"]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Debug2</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">SplitToTailwindClassNames</span><span class="token operator">&lt;</span><span class="token string">'block p-1'</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// type Debug3 = ["block", "p-1"]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Debug3</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">SplitToTailwindClassNames</span><span class="token operator">&lt;</span><span class="token string">'block\n\np-1   '</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// Still not working:</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// Whitespace at the beginning</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Debug4</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">SplitToTailwindClassNames</span><span class="token operator">&lt;</span><span class="token string">' block'</span><span class="token operator">&gt;</span><span class="token plain">  </span><span class="token comment">// []</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// Errors are not dealt correctly,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// should be [never]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Debug5</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">SplitToTailwindClassNames</span><span class="token operator">&lt;</span><span class="token string">'invalid'</span><span class="token operator">&gt;</span><span class="token plain">  </span><span class="token comment">// []</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">The easiest way to fix the leading whitespace issue is to make a wrapper utility that calls <code>Trim</code>:</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">SplitToTailwindClassNames</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name"> </span><span class="token class-name keyword">extends</span><span class="token class-name"> </span><span class="token class-name builtin">string</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">SplitToTailwindClassNamesInner</span><span class="token operator">&lt;</span><span class="token maybe-class-name">Trim</span><span class="token operator">&lt;</span><span class="token constant">T</span><span class="token operator">&gt;&gt;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">The error handling can be improved by using the <code>Err</code> type and separating the "invalid T" and "end recursion" cases (<a href="https://www.typescriptlang.org/play?#code/C4TwDgpgBAwgNgQwM5IHIILbQLwCgpQA+UA5AEZwD2AxgNYn5EAFJAlgHZwcQC0FN9RsRIAzOBAAeDAsIDmAJ1YATaUVJlZPBQhA8AjAAYDq4WH2qA9BagA6O1ARRgAC0oBXJAnZKoGSvIhcXFBIKABJdiRgL2BWBFjKdgARCDAXACUIJTdqDlkAdWdWYAgkMARqHCFSAlJqkgITGqaSAB12BmDwaAAVRQwAZWj5YAAeHqhJEu8kKCjFdlkAPig8AgmpiBmoAAMAEgBvCKiYuITk1IysnLzC4tLyyoBfQ44RCHkoHoRWOCedxgEAD8X36QwQI3GPzgS0BUAAXF8giFev0AKLecaTCTTJSzeZ5FZrL7Y3GzfYHN4fKAABQCIlYEheR0i0XYsXirESKTSzky2VyizuJTKFQg-zhIL6rAwGKUozpEAZElhtQRSK6oWlGCxm22BMWRNBMrl4zBwzGPSWsM10AGYC4wB6lG+vwA7hwlPBkGhMKVdTitni5sAFstVlB7Y7na64B7vN6UOgsEgIuwPmaZeNrUErFAAKqxR0gJzdJyUKBgeSUABuymg7FYlU+H2rnxTnlkpVtUDR8nkowAsqVO9A9cGDeHsLs+23EYdhygEF2JT2o8UY9D417EEm-an2OmBxtA-rQ4TVowT2TdodE76sMyqZ9YxK1VLSUHya92O9PjBmVfAE1WBKAAG0YAAGlsOx1ydF0t09e9k39bUoV+a0AF04QIRFZ1GEgBlcNw4B8dhKGAKBnAQMBIA6G01TzABBJQlCySZ+38KivCULhFjhPMAAkePEWZqGQUooDdZwPmgHYeh2KAlEoSTyMojB4moZwBOsO9dwfcVDlfGwoAAMS4yRMAdQJGOscgqDoEhoJIJzSHadpXLYdgawQLgVGgrivJ8vyCH4RydJ0qBUAoiBEXk6FFOo2ZfICBAlBLMgIC2Jx+iwHwRGrDApKKEVHhs2pEWvL9b0pX9qSAnCoClaFP22ZC-Uagg83i35FIgABHNxfOSqBguUL5EO8KBqH0mxOqgPM5SgAJqDceQkC5dg5hySokBEEi4BAOaQNqEEwNjbCTtw40dVjFZx1mFz5q66wergRLkCgRJDuK+5RUqKBxBEYBjqul7eymla1o2xJtuoXb9rgQ7QbBs7LrBhbrAGSgsBcPJJjgJBoDdT6RHcbwUauvN0h+ImHC21t-Ge9UwPwnYSCM6EngaVhZjUhxRt88bY23ab9J2JZsMavMiPcUioHTGtqWo2jss++TFN5gXJ0p67Wf7AjZZIsiKO41X6KlvMUSgFIyDcWQ9AjMCACIwtoZ3sOt237cd6c4M3d0kP0lCkAIt2SFhK2y292QACYnddhz3eg52zD0D2exj+O-YdDcEMDhNg-3MOk8rcxI+sL2IDt2QAGYE7d52U7TjOq5r+uc+jfO4yDn0Q5LgR3PYNPagj3Ao9CGOO-AxOBFb6Pq-tgAWCN-e77d2pTAioHDivS0nxfZAAVgT2d-ERIKhZUKAtf5xwxp8EXPTFn154PmuT87vOn8Lvvi8vvyEcoBAA" class="cursor-pointer underline hover:underline ">TypeScript playground</a>):</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">SplitToTailwindClassNamesInner</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name"> </span><span class="token class-name keyword">extends</span><span class="token class-name"> </span><span class="token class-name builtin">string</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token constant">T</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">ClassName</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">infer Tail</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">?</span><span class="token plain"> </span><span class="token constant">T</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">infer </span><span class="token template-string interpolation constant">C</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">Tail</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token operator">?</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token constant">C</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token spread operator">...</span><span class="token maybe-class-name">SplitToTailwindClassNames</span><span class="token operator">&lt;</span><span class="token maybe-class-name">Trim</span><span class="token operator">&lt;</span><span class="token maybe-class-name">Tail</span><span class="token operator">&gt;&gt;</span><span class="token punctuation">]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">Err</span><span class="token operator">&lt;</span><span class="token string">'Should not happen'</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// Added error handling</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// Handles cases where `T` does not match</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// ${ClassName}${Tail}. For example</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// 'block', '', '\n\n', 'invalid', or 'invalid  block'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">//</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// Note: `Tail` has already been trimmed from whitespace</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">:</span><span class="token plain"> </span><span class="token constant">T</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">infer Tail</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token operator">?</span><span class="token plain"> </span><span class="token maybe-class-name">Tail</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token class-name maybe-class-name">ClassName</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">        </span><span class="token comment">// `Tail` equals a valid Tailwind class.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">        </span><span class="token comment">// End recursion succesfully.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">        </span><span class="token operator">?</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token maybe-class-name">Tail</span><span class="token punctuation">]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">        </span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">Trim</span><span class="token operator">&lt;</span><span class="token maybe-class-name">Tail</span><span class="token operator">&gt;</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token string">''</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">          </span><span class="token comment">// `Tail` has only whitespace left.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">          </span><span class="token comment">// End recursion succesfully.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">          </span><span class="token operator">?</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">          </span><span class="token comment">// Something else was found.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">          </span><span class="token comment">// Raise an error</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">          </span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token maybe-class-name">Err</span><span class="token operator">&lt;</span><span class="token template-string template-punctuation string">`</span><span class="token template-string string">'</span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">Tail</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string string">' is not a valid Tailwind class</span><span class="token template-string template-punctuation string">`</span><span class="token operator">&gt;</span><span class="token punctuation">]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">      </span><span class="token comment">// Should never happen as `T` is a string.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token maybe-class-name">Err</span><span class="token operator">&lt;</span><span class="token string">'Should not happen'</span><span class="token operator">&gt;</span><span class="token punctuation">]</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Finally we got to a version that handles all the cases as expected.</p><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="5-string-type">5. String type</h3><p class="max-w-xl font-sans mb-paragraph ">Let's turn the pieces above into a utility which validates that a string contains only valid Tailwind classes. The utility type only works if the string is a literal type such as <code>"p-1 block"</code>. A generic <code>string</code> type won't suffice, because template literals operate on string constants.</p><p class="max-w-xl font-sans mb-paragraph ">As the first step, let's validate that the input indeed is a literal string type.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">IsValidTailwindClassString</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name"> </span><span class="token class-name keyword">extends</span><span class="token class-name"> </span><span class="token class-name builtin">string</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token builtin">string</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token class-name constant">T</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">?</span><span class="token plain"> </span><span class="token maybe-class-name">Err</span><span class="token operator">&lt;</span><span class="token string">'Unexpected generic string'</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// Make sure each item is a valid `ClassName`</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">SplitToTailwindClassNames</span><span class="token operator">&lt;</span><span class="token constant">T</span><span class="token operator">&gt;</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token class-name maybe-class-name">ClassName</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token comment">// If yes, success and return the type `T` itself</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token operator">?</span><span class="token plain"> </span><span class="token constant">T</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token comment">// If no, raise an error</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">Err</span><span class="token operator">&lt;</span><span class="token string">'Invalid Tailwind class string'</span><span class="token operator">&gt;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Now when the utility is invoked with a generic <code>string</code>, we get an error:</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token comment">// INCORRECT</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> str</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string">'generic string'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// type Debug = "Error: Unexpected generic string"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Debug</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">IsValidTailwindClassString</span><span class="token operator">&lt;</span><span class="token keyword">typeof</span><span class="token plain"> str</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// CORRECT</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> str2 </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string">'p-1'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Debug2</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">IsValidTailwindClassString</span><span class="token operator">&lt;</span><span class="token keyword">typeof</span><span class="token plain"> str2</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// type Debug2 = "p-1"</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Also, we correctly get an error in case there's an invalid class name:</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token comment">// type Debug = "Error: Invalid Tailwind class string"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Debug</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">IsValidTailwindClassString</span><span class="token operator">&lt;</span><span class="token template-string template-punctuation string">`</span><span class="token template-string string"></span></div><div class="token-line"><span class="token template-string string">  block</span></div><div class="token-line"><span class="token template-string string">  bg-green-10c</span></div><div class="token-line"><span class="token template-string string"></span><span class="token template-string template-punctuation string">`</span><span class="token operator">&gt;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">That's already awesome, but let's change it to show which tokens where invalid for a better developer experience. We could for example return the actual array that contains the correct Tailwind classes and the error (<a href="https://www.typescriptlang.org/play?#code/C4TwDgpgBAwgNgQwM5IHIILbQLwCgpQA+UA5AEZwD2AxgNYn5EAFJAlgHZwcQC0FN9RsRIAzOBAAeDAsIDmAJ1YATaUVJlZPBQhA8AjAAYDq4WH2qA9BagA6O1ARRgAC0oBXJAnZKoGSvIhcXFBIKABJdiRgL2BWBFjKdgARCDAXACUIJTdqDlkAdWdWYAgkMARqHCFSAlJqkgITGqaSAB12BmDwaAAVRQwAZWj5YAAeHqhJEu8kKCjFdlkAPig8AgmpiBmoAAMAEgBvCKiYuITk1IysnLzC4tLyyoBfQ44RCHkoHoRWOCedxgEAD8X36QwQI3GPzgS0BUAAXF8giFev0AKLecaTCTTJSzeZ5FZrL7Y3GzfYHN4fKAABQCIlYEheR0i0XYsXirESKTSzky2VyizuJTKFQg-zhIL6rAwGKUozpEAZElhtQRSK6oWlGCxm22BMWRNBMrl4zBwzGPSWsM10AGYC4wB6lG+vwA7hwlPBkGhMKVdTitni5sAFstVlB7Y7na64B7vN6UOgsEgIuwPmaZeNrUErFAAKqxR0gJzdJyUKBgeSUABuymg7FYlU+H2rnxTnlkpVtUDR8nkowAsqVO9A9cGDeHsLs+23EYdhygEF2JT2o8UY9D417EEm-an2OmBxtA-rQ4TVowT2TdodE76sMyqZ9YxK1VLSUHya92O9PjBmVfAE1WBKAAG0YAAGlsOx1ydF0t09e9k39bUoV+a0AF04QIRFZ1GEgBlcNw4B8dhKGAKBnAQMBIA6G01TzABBJQlCySZ+38KivCULhFjhPMAAkePEWZqGQUooDdZwPmgHYeh2KAlEoSTyMojB4moZwBOsO9dwfcVDlfGwoAAMS4yRMAdQJGOscgqDoEhoJIJzSHadpXLYdgawQLgVGgrivJ8vyCH4RydJ0qBUAoiBEXk6FFOo2ZfICBAlBLMgIC2Jx+iwHwRGrDApKKEVHhs2pEWvL9b0pX9qSAnCoClaFP22ZC-Uagg83i35FIgABHNxfOSqBguUL5EO8KBqH0mxOqgPM5SgAJqDceQkC5dg5hySokBEEi4BAOaQNqEEwNjbCTtw40dVjFZx1mFz5q66wergRLkCgRJDuK+5RUqKBxBEYBjqul7eymla1o2xJtuoXb9rgQ7QbBs7LrBhbrAGSgsBcPJJjgJBoDdT6RHcbwUauvN0h+ImHC21t-Ge9UwPwnYSCM6EngaVhZjUhxRt88bY23ab9J2JZsMavMiPcUioHTGtqWo2jss++TFN5gXJ0p67Wf7AjZZIsiKO41X6KlnswiQAA1IWlBFpD9KGMMAxvSciUYSdWuDHpJV7A2SHzdMJEgagSh8LsjybEMwxIVVajzQcEFoaAkDWscKmcKB7iKrXHDGnwdnarBgIqyMHQ3BD3Sdn0UKQbMfdmEuIDA9HbPCEQoBAUpoPT+GR3pnwAmANatpcaAUV2BSc+AIm4BERqpWl6wwi78joPkWnoC8Di20axE4M3GuE30+vs1zawp5SMg3FkPQIwAIjC2hH57G+74f6drbtvzHdPn0Ls8gERfvHXAeZr4QFvrIAATE-F+lZ9Bv0gdAuB39bb23-juQB55FggIcrQRBegwEQLLB-WQABmeBBD3LsDMA-AgyCyFQLvlQ9Bv9lBYPvEAvB9kBC0PobUEhV9mHQIACxPygC-JhoRyESPYZgya2CUA8NkARKRBDhGllkSw2QABWCMYFn4EMftBR+9DTFQEfrOfwiIgr2x5nzU2Bd7YTRPj4GaPpH7YRQXfAxCi-5KO4bgtRfC6BEJzt5BxKwgA" class="cursor-pointer underline hover:underline ">TypeScript playground</a>).</p><pre class="py-0"><code class="language-diff"><div class="token-line"><span class="token unchanged prefix unchanged"> </span><span class="token unchanged line">type IsValidTailwindClassString&lt;T extends string&gt; =</span></div><div class="token-line"><span class="token unchanged line"></span><span class="token unchanged prefix unchanged"> </span><span class="token unchanged line">  string extends T</span></div><div class="token-line"><span class="token unchanged line"></span><span class="token unchanged prefix unchanged"> </span><span class="token unchanged line">    ? Err&lt;'Unexpected generic string'&gt;</span></div><div class="token-line"><span class="token unchanged line"></span><span class="token unchanged prefix unchanged"> </span><span class="token unchanged line">    // Make sure each item is a valid `ClassName`</span></div><div class="token-line"><span class="token unchanged line"></span><span class="token unchanged prefix unchanged"> </span><span class="token unchanged line">    : SplitToTailwindClassNames&lt;T&gt; extends ClassName[]</span></div><div class="token-line"><span class="token unchanged line"></span><span class="token unchanged prefix unchanged"> </span><span class="token unchanged line">      // If yes, success and return the type `T` itself</span></div><div class="token-line"><span class="token unchanged line"></span><span class="token unchanged prefix unchanged"> </span><span class="token unchanged line">      ? T</span></div><div class="token-line"><span class="token unchanged line"></span><span class="token unchanged prefix unchanged"> </span><span class="token unchanged line">      // If no, raise an error</span></div><div class="token-line"><span class="token unchanged line"></span><span class="token deleted-sign deleted prefix deleted">-</span><span class="token deleted-sign deleted line">      : Err&lt;'Invalid Tailwind class string'&gt;</span></div><div class="token-line"><span class="token deleted-sign deleted line"></span><span class="token inserted-sign inserted prefix inserted">+</span><span class="token inserted-sign inserted line">      : SplitToTailwindClassNames&lt;T&gt;</span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">It would work, but the array format can contain valid class names which would make the actual error message harder to spot.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token comment">// type Debug5 = ["block", "p-1", "Error: 'invalid' is not a valid Tailwind class"]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Debug5</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">IsValidTailwindClassString</span><span class="token operator">&lt;</span><span class="token string">'block p-1 invalid'</span><span class="token operator">&gt;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">To fix that, we can get the first error from the array. Fortunately we're dealing with just a flat array!</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token comment">// Gets the first string of an array that starts with 'Error: '</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// Must be used only when `T` actually includes an error item</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name known-class-name class-name">GetFirstError</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name"> </span><span class="token class-name keyword">extends</span><span class="token class-name"> </span><span class="token class-name builtin">unknown</span><span class="token class-name punctuation">[</span><span class="token class-name punctuation">]</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token constant">T</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token plain">infer </span><span class="token maybe-class-name">Head</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token spread operator">...</span><span class="token plain">infer </span><span class="token maybe-class-name">Tail</span><span class="token punctuation">]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">?</span><span class="token plain"> </span><span class="token maybe-class-name">Head</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token template-string template-punctuation string">`</span><span class="token template-string string">Error: </span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">infer Message</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string template-punctuation string">`</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token comment">// Match found, return</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token operator">?</span><span class="token plain"> </span><span class="token maybe-class-name">Head</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token comment">// Continue searching for an error string</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token operator">:</span><span class="token plain"> </span><span class="token known-class-name class-name">GetFirstError</span><span class="token operator">&lt;</span><span class="token maybe-class-name">Tail</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">never</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Let's add it to the string-validating utility and call it done (<a href="https://www.typescriptlang.org/play?#code/C4TwDgpgBAwgNgQwM5IHIILbQLwCgpQA+UA5AEZwD2AxgNYn5EAFJAlgHZwcQC0FN9RsRIAzOBAAeDAsIDmAJ1YATaUVJlZPBQhA8AjAAYDq4WH2qA9BagA6O1ARRgAC0oBXJAnZKoGSvIhcXFBIKABJdiRgL2BWBFjKdgARCDAXACUIJTdqDlkAdWdWYAgkMARqHCFSAlJqkgITGqaSAB12BmDwaAAVRQwAZWj5YAAeHqhJEu8kKCjFdlkAPig8AgmpiBmoAAMAEgBvCKiYuITk1IysnLzC4tLyyoBfQ44RCHkoHoRWOCedxgEAD8X36QwQI3GPzgS0BUAAXF8giFev0AKLecaTCTTJSzeZ5FZrL7Y3GzfYHN4fKAABQCIlYEheR0i0XYsXirESKTSzky2VyizuJTKFQg-zhIL6rAwGKUozpEAZElhtQRSK6oWlGCxm22BMWRNBMrl4zBwzGPSWsM10AGYC4wB6lG+vwA7hwlPBkGhMKVdTitni5sAFstVlB7Y7na64B7vN6UOgsEgIuwPmaZeNrUErFAAKqxR0gJzdJyUKBgeSUABuymg7FYlU+H2rnxTnlkpVtUDR8nkowAsqVO9A9cGDeHsLs+23EYdhygEF2JT2o8UY9D417EEm-an2OmBxtA-rQ4TVowT2TdodE76sMyqZ9YxK1VLSUHya92O9PjBmVfAE1WBKAAG0YAAGlsOx1ydF0t09e9k39bUoV+a0AF04QIRFZ1GEgBlcNw4B8dhKGAKBnAQMBIA6G01TzABBJQlCySZ+38KivCULhFjhPMAAkePEWZqGQUooDdZwPmgHYeh2KAlEoSTyMojB4moZwBOsO9dwfcVDlfGwoAAMS4yRMAdQJGOscgqDoEhoJIJzSHadpXLYdgawQLgVGgrivJ8vyCH4RydJ0qBUAoiBEXk6FFOo2ZfICBAlBLMgIC2Jx+iwHwRGrDApKKEVHhs2pEWvL9b0pX9qSAnCoClaFP22ZC-Uagg83i35FIgABHNxfOSqBguUL5EO8KBqH0mxOqgPM5SgAJqDceQkC5dg5hySokBEEi4BAOaQNqEEwNjbCTtw40dVjFZx1mFz5q66wergRLkCgRJDuK+5RUqKBxBEYBjqul7eymla1o2xJtuoXb9rgQ7QbBs7LrBhbrAGSgsBcPJJjgJBoDdT6RHcbwUauvN0h+ImHC21t-Ge9UwPwnYSCM6EngaVhZjUhxRt88bY23ab9J2JZsMavMiPcUioHTGtqWo2jss++TFN5gXJ0p67Wf7AjZZIsiKO41X6KlvMAHEIGAWYXGgBl1soycvpEemHH7HQnGol2LVmD0XFIWd-ERBg80HDxKMyqAPHY76S2k7KNYcahgCGpGSw4Ga3DY5KGc4z57gwHsbeAUzWGdkPj1a4M3HYWhyLddgwMwokr1r2YwOfKBBIgNLoLsGwe4uyVe-7nwHpnQv51qv8oEXUc3xAiPNOcKAyfrpRoICdP5HYRqQT7tLpesGBElidg3GgImIS0-Gyc+LwOLbEMw0axEy4rqvC-QmE4URIrD4yIyxhCQAANSFkoEWSF9JDDDAGG8k524EFdlPHoY98IkHzOmCQkA07sS7EeJsb88gkFVLUVetAb5rTHBUdexcoBa0cGNHwOx2pYGAhVSMDoNwIXdLAn0KEkDZk7rAfSKFW6n3CO7EApRoJIB2iOemPhd5rS2g7UsoQU7FCJnAEQh8kQnTzGEd25Ed602gM-Rm8gP5QC-pXKI1dRhwU3AIhMEj9zZhtHmFEUAUhkDcLIPQEYABEYVaChJ7AEoJITpxgMgX5GBHifTwLyARCJ5DcC+LLDE2QAAmMJETKz6CiX4vJhT4kQKgckncqTzyLAyQ5WgJS9BZJyaEPJABmIpzT3LsDMCEggZTckQECbIHpVTEnKFqfeNJjT7ICH6YM2o7TrDlLGUEgALGEqAESRmdM2bIHZUyamTTqSgeZsgCJ7OaWsrR0A8kAFYwnVzDhwVhPM+amxYVAia7ifAzR9Acx5RyXmnKSecuZDTrmLLoK0ph3koHkKgEAA" class="cursor-pointer underline hover:underline ">TypeScript playground</a>).</p><pre class="py-0"><code class="language-diff"><div class="token-line"><span class="token unchanged prefix unchanged"> </span><span class="token unchanged line">type IsValidTailwindClassString&lt;T extends string&gt; =</span></div><div class="token-line"><span class="token unchanged line"></span><span class="token unchanged prefix unchanged"> </span><span class="token unchanged line">  string extends T</span></div><div class="token-line"><span class="token unchanged line"></span><span class="token unchanged prefix unchanged"> </span><span class="token unchanged line">    ? Err&lt;'Unexpected generic string'&gt;</span></div><div class="token-line"><span class="token unchanged line"></span><span class="token unchanged prefix unchanged"> </span><span class="token unchanged line">    // Make sure each item is a valid `ClassName`</span></div><div class="token-line"><span class="token unchanged line"></span><span class="token unchanged prefix unchanged"> </span><span class="token unchanged line">    : SplitToTailwindClassNames&lt;T&gt; extends ClassName[]</span></div><div class="token-line"><span class="token unchanged line"></span><span class="token unchanged prefix unchanged"> </span><span class="token unchanged line">      // If yes, success and return the type `T` itself</span></div><div class="token-line"><span class="token unchanged line"></span><span class="token unchanged prefix unchanged"> </span><span class="token unchanged line">      ? T</span></div><div class="token-line"><span class="token unchanged line"></span><span class="token unchanged prefix unchanged"> </span><span class="token unchanged line">      // If no, raise an error</span></div><div class="token-line"><span class="token unchanged line"></span><span class="token deleted-sign deleted prefix deleted">-</span><span class="token deleted-sign deleted line">      : Err&lt;'Invalid Tailwind class string'&gt;</span></div><div class="token-line"><span class="token deleted-sign deleted line"></span><span class="token inserted-sign inserted prefix inserted">+</span><span class="token inserted-sign inserted line">      : GetFirstError&lt;SplitToTailwindClassNames&lt;T&gt;&gt;</span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">After the change, the error looks as expected.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token comment">// type Debug5 = "Error: 'invalid' is not a valid Tailwind class"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Debug5</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">IsValidTailwindClassString</span><span class="token operator">&lt;</span><span class="token string">'block p-1 invalid'</span><span class="token operator">&gt;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Everything we've built so far has been now combined into a single utility type called <code>IsValidTailwindClassString</code>. That was a lot of work, but we still have a few other formats to cover.</p><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="6-object-type">6. Object type</h3><p class="max-w-xl font-sans mb-paragraph ">In the object format, each key is a string of class name<em class="italic">(s)</em> and the value is a truthy or falsy value indicating if the class names should be included in the final class name.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">const</span><span class="token plain"> dense </span><span class="token operator">=</span><span class="token plain"> </span><span class="token boolean">true</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> classNames </span><span class="token operator">=</span><span class="token plain"> </span><span class="token function">cn</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token string">'p-1 m-1'</span><span class="token operator">:</span><span class="token plain"> dense</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token string">'p-3'</span><span class="token operator">:</span><span class="token plain"> </span><span class="token operator">!</span><span class="token plain">dense</span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// classNames = "p-1 m-1"</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">We need to iterate through each key-value pair in the object, and validate that the key consists of valid Tailwind classes. "Iterating" objects in TypeScript can be done with <a href="https://www.typescriptlang.org/docs/handbook/2/mapped-types.html" class="cursor-pointer underline hover:underline ">mapped types</a>. Let's look at a simpler example where we convert a given input object type values to nullables.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">MakeNullable</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token comment">// "For each key (K) in object (T)"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">[</span><span class="token constant">K</span><span class="token plain"> </span><span class="token keyword">in</span><span class="token plain"> </span><span class="token keyword">keyof</span><span class="token plain"> </span><span class="token constant">T</span><span class="token punctuation">]</span><span class="token operator">:</span><span class="token plain"> </span><span class="token constant">T</span><span class="token punctuation">[</span><span class="token constant">K</span><span class="token punctuation">]</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token keyword">null</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Now we can use <code>MakeNullable</code> to make all object's values nullable (<a href="https://www.typescriptlang.org/play?#code/C4TwDgpgBAkgdmArsKBeKBvAUFKBDALijkQFsAjCAJwBocpyjyB7ZgGwjzjoF8stQkKAFk8AawgA5RGzZ5yHADwAVAHxpM9ANoBpKAEs4UCSGYAzKMoC6RZbqtQAPsRlssfAeGgAlCAGcZFHRRCWlZeSV4JGBVIA" class="cursor-pointer underline hover:underline ">TypeScript playground</a>).</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Input</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> a</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation">,</span><span class="token plain"> b</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">boolean</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// type Result = {</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">//   a: number | null;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">//   b: boolean | null;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// }</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Result</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">MakeNullable</span><span class="token operator">&lt;</span><span class="token maybe-class-name">Input</span><span class="token operator">&gt;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Using the knowledge, it's not a huge leap to validating Tailwind class names in keys. We already have <code>IsValidTailwindClassString</code> utility type which checks if a given string consists of valid Tailwind classes or not.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token comment">// Use `any` because classnames uses truthiness check,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// and does not require booleans</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">TailwindClassNamesObject</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name"> </span><span class="token class-name keyword">extends</span><span class="token class-name"> </span><span class="token class-name punctuation">{</span><span class="token class-name"> </span><span class="token class-name punctuation">[</span><span class="token class-name">key</span><span class="token class-name operator">:</span><span class="token class-name"> </span><span class="token class-name builtin">string</span><span class="token class-name punctuation">]</span><span class="token class-name operator">:</span><span class="token class-name"> </span><span class="token class-name builtin">any</span><span class="token class-name"> </span><span class="token class-name punctuation">}</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">[</span><span class="token constant">K</span><span class="token plain"> </span><span class="token keyword">in</span><span class="token plain"> </span><span class="token keyword">keyof</span><span class="token plain"> </span><span class="token constant">T</span><span class="token punctuation">]</span><span class="token operator">:</span><span class="token plain"> </span><span class="token constant">K</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token class-name maybe-class-name">IsValidTailwindClassString</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">K</span><span class="token class-name operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">?</span><span class="token plain"> </span><span class="token constant">T</span><span class="token punctuation">[</span><span class="token constant">K</span><span class="token punctuation">]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// If invalid class found, return the error</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">IsValidTailwindClassString</span><span class="token operator">&lt;</span><span class="token constant">K</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">It's that simple! Well, almost.. If you look at the type in <a href="https://www.typescriptlang.org/play?#code/C4TwDgpgBAwgNgQwM5IHIILbQLwCgpQA+UA5AEZwD2AxgNYn5EAFJAlgHZwcQC0FN9RsRIAzOBAAeDAsIDmAJ1YATaUVJlZPBQhA8AjAAYDq4WH2qA9BagA6O1ARRgAC0oBXJAnZKoGSvIhcXFBIKABJdiRgL2BWBFjKdgARCDAXACUIJTdqDlkAdWdWYAgkMARqHCFSAlJqkgITGqaSAB12BmDwaAAVRQwAZWj5YAAeHqhJEu8kKCjFdlkAPig8AgmpiBmoAAMAEgBvCKiYuITk1IysnLzC4tLyyoBfQ44RCHkoHoRWOCedxgEAD8X36QwQI3GPzgS0BUAAXF8giFev0AKLecaTCTTJSzeZ5FZrL7Y3GzfYHN4fKAABQCIlYEheR0i0XYsXirESKTSzky2VyizuJTKFQg-zhIL6rAwGKUozpEAZElhtQRSK6oWlGCxm22BMWRNBMrl4zBwzGPSWsM10AGYC4wB6lG+vwA7hwlPBkGhMKVdTitni5sAFstVlB7Y7na64B7vN6UOgsEgIuwPmaZeNrUErFAAKqxR0gJzdJyUKBgeSUABuymg7FYlU+H2rnxTnlkpVtUDR8nkowAsqVO9A9cGDeHsLs+23EYdhygEF2JT2o8UY9D417EEm-an2OmBxtA-rQ4TVowT2TdodE76sMyqZ9YxK1VLSUHya92O9PjBmVfAE1WBKAAG0YAAGlsOx1ydF0t09e9k39bUoV+a0AF04QIRFZ1GEgBlcNw4B8dhKGAKBnAQMBIA6G01TzABBJQlCySZ+38KivCULhFjhPMAAkePEWZqGQUooDdZwPmgHYeh2KAlEoSTyMojB4moZwBOsO9dwfcVDlfGwoAAMS4yRMAdQJGOscgqDoEhoJIJzSHadpXLYdgawQLgVGgrivJ8vyCH4RydJ0qBUAoiBEXk6FFOo2ZfICBAlBLMgIC2Jx+iwHwRGrDApKKEVHhs2pEWvL9b0pX9qSAnCoClaFP22ZC-Uagg83i35FIgABHNxfOSqBguUL5EO8KBqH0mxOqgPM5SgAJqDceQkC5dg5hySokBEEi4BAOaQNqEEwNjbCTtw40dVjFZx1mFz5q66wergRLkCgRJDuK+5RUqKBxBEYBjqul7eymla1o2xJtuoXb9rgQ7QbBs7LrBhbrAGSgsBcPJJjgJBoDdT6RHcbwUauvN0h+ImHC21t-Ge9UwPwnYSCM6EngaVhZjUhxRt88bY23ab9J2JZsMavMiPcUioHTGtqWo2jss++TFN5gXJ0p67Wf7AjZZIsiKO41X6KlvMAHEIGAWYXGgBl1soycvpEemHH7HQnGol2LVmD0XFIWd-ERBg80HDxKMyqAPHY76S2k7KNYcahgCGpGSw4Ga3DY5KGc4z57gwHsbeAUzWGdkPj1a4M3HYWhyLddgwMwokr1r2YwOfKBBIgNLoLsGwe4uyVe-7nwHpnQv51qv8oEXUc3xAiPNOcKAyfrpRoICdP5HYRqQT7tLpesGBElidg3GgImIS0-Gyc+LwOLbEMw0axEy4rqvC-QmE4URIrD4yIyxhCQAANSFkoEWSF9JDDDAGG8k524EFdlPHoY98IkHzOmCQkA07sS7EeJsb88gkFVLUVetAb5rTHBUdexcoBa0cGNHwOx2pYGAhVSMDoNwIXdLAn0KEkDZk7rAfSKFW6n3CO7EApRoJIB2iOemPhd5rS2g7UsoQU7FCJnAEQh8kQnTzGEd25Ed602gM-Rm8gP5QC-pXKI1dRhwU3AIhMEj9zZhtHmfMdMdheBAIpTK4k45ix9Owfcscib23kG4PG6YUDTRknQSCuA8w8SUipPmpsAiDUrtAAAVlHKAZBKCUHEF4JAPYYEeKEfuAA8mQQpEA06IOqgccC1CQCIknJhREgSoBPCNAcRgYEADSTCtrdMoO7Ho-SoCTKnmAyBflak7h9PAvIoxxkUNAj0CZ6NwamKmaw8JSTN7eB3rbdRPsxyFwAeECBUD1n3i2YsHZsInhBCAA" class="cursor-pointer underline hover:underline ">TypeScript playground</a>, you can see the following error.</p><pre class="py-0"><code class="language-no-highlight"><div class="token-line"><span class="token plain">Type 'K' does not satisfy the constraint 'string'.</span></div><div class="token-line"><span class="token plain">  Type 'keyof T' is not assignable to type 'string'.</span></div><div class="token-line"><span class="token plain">    Type 'string | number | symbol' is not assignable to type 'string'.</span></div><div class="token-line"><span class="token plain">      Type 'number' is not assignable to type 'string'.(2344)</span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Even when we explicitly defined that the input object can have only <code>string</code> keys, TypeScript will complain about it. The workaround provided in <a href="https://github.com/microsoft/TypeScript/pull/40336#issuecomment-717319022" class="cursor-pointer underline hover:underline ">this PR comment</a> is to use <code>&amp; string</code> to filter out all non-string named properties.</p><p class="max-w-xl font-sans mb-paragraph ">Fortunately the trick works like a charm (<a href="https://www.typescriptlang.org/play?#code/C4TwDgpgBAwgNgQwM5IHIILbQLwCgpQA+UA5AEZwD2AxgNYn5EAFJAlgHZwcQC0FN9RsRIAzOBAAeDAsIDmAJ1YATaUVJlZPBQhA8AjAAYDq4WH2qA9BagA6O1ARRgAC0oBXJAnZKoGSvIhcXFBIKABJdiRgL2BWBFjKdgARCDAXACUIJTdqDlkAdWdWYAgkMARqHCFSAlJqkgITGqaSAB12BmDwaAAVRQwAZWj5YAAeHqhJEu8kKCjFdlkAPig8AgmpiBmoAAMAEgBvCKiYuITk1IysnLzC4tLyyoBfQ44RCHkoHoRWOCedxgEAD8X36QwQI3GPzgS0BUAAXF8giFev0AKLecaTCTTJSzeZ5FZrL7Y3GzfYHN4fKAABQCIlYEheR0i0XYsXirESKTSzky2VyizuJTKFQg-zhIL6rAwGKUozpEAZElhtQRSK6oWlGCxm22BMWRNBMrl4zBwzGPSWsM10AGYC4wB6lG+vwA7hwlPBkGhMKVdTitni5sAFstVlB7Y7na64B7vN6UOgsEgIuwPmaZeNrUErFAAKqxR0gJzdJyUKBgeSUABuymg7FYlU+H2rnxTnlkpVtUDR8nkowAsqVO9A9cGDeHsLs+23EYdhygEF2JT2o8UY9D417EEm-an2OmBxtA-rQ4TVowT2TdodE76sMyqZ9YxK1VLSUHya92O9PjBmVfAE1WBKAAG0YAAGlsOx1ydF0t09e9k39bUoV+a0AF04QIRFZ1GEgBlcNw4B8dhKGAKBnAQMBIA6G01TzABBJQlCySZ+38KivCULhFjhPMAAkePEWZqGQUooDdZwPmgHYeh2KAlEoSTyMojB4moZwBOsO9dwfcVDlfGwoAAMS4yRMAdQJGOscgqDoEhoJIJzSHadpXLYdgawQLgVGgrivJ8vyCH4RydJ0qBUAoiBEXk6FFOo2ZfICBAlBLMgIC2Jx+iwHwRGrDApKKEVHhs2pEWvL9b0pX9qSAnCoClaFP22ZC-Uagg83i35FIgABHNxfOSqBguUL5EO8KBqH0mxOqgPM5SgAJqDceQkC5dg5hySokBEEi4BAOaQNqEEwNjbCTtw40dVjFZx1mFz5q66wergRLkCgRJDuK+5RUqKBxBEYBjqul7eymla1o2xJtuoXb9rgQ7QbBs7LrBhbrAGSgsBcPJJjgJBoDdT6RHcbwUauvN0h+ImHC21t-Ge9UwPwnYSCM6EngaVhZjUhxRt88bY23ab9J2JZsMavMiPcUioHTGtqWo2jss++TFN5gXJ0p67Wf7AjZZIsiKO41X6KlvMAHEIGAWYXGgBl1soycvpEemHH7HQnGol2LVmD0XFIWd-ERBg80HDxKMyqAPHY76S2k7KNYcahgCGpGSw4Ga3DY5KGc4z57gwHsbeAUzWGdkPj1a4M3HYWhyLddgwMwokr1r2YwOfKBBIgNLoLsGwe4uyVe-7nwHpnQv51qv8oEXUc3xAiPNOcKAyfrpRoICdP5HYRqQT7tLpesGBElidg3GgImIS0-Gyc+LwOLbEMw0axEy4rqvC-QmE4URIrD4yIyxhCQAANSFkoEWSF9JDDDAGG8k524EFdlPHoY98IkHzOmCQkA07sS7EeJsb88gkFVLUVetAb5rTHBUdexcoBa0cGNHwOx2pYGAhVSMDoNwIXdLAn0KEkDZk7rAfSKFW6n3CO7EApRoJIB2iOemPhd5rS2g7UsoQU7FCJnAEQh8kQnTzGEd25Ed602gM-Rm8gP5QC-pXKI1dRhwU3AIhMEj9zZhtHmfMdMdheBAIpTK4k45ix9Owfcscib23kG4PG6YUDTRknQSCuA8w8SUipPmpsAiDUrtAAAVlHKAZBKCUHEF4JAPYYEeKEfuAA8mQQpEA06IOqgccC1CQCIknJhREgSoBPCNAcRgYEADSTCtrdMoO7CYAAyUhix+lQEmVPMBkC-K1J3D6eBeRRjjIoaBHoEz0bg1MVM1h4Skmb28DvW26ifZjkLgA8IECoHbPvHsxYBzYRPBAaETIii4DAD0BGT5niUxNJaW00ZjQzB6BIIiUM180mNDEJIKQiIRDDQgGk4ZQA" class="cursor-pointer underline hover:underline ">TypeScript playground</a>).</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token comment">// Use `any` because classnames uses truthiness check,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// and does not require just booleans</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">TailwindClassNamesObject</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name"> </span><span class="token class-name keyword">extends</span><span class="token class-name"> </span><span class="token class-name punctuation">{</span><span class="token class-name"> </span><span class="token class-name punctuation">[</span><span class="token class-name">key</span><span class="token class-name operator">:</span><span class="token class-name"> </span><span class="token class-name builtin">string</span><span class="token class-name punctuation">]</span><span class="token class-name operator">:</span><span class="token class-name"> </span><span class="token class-name builtin">any</span><span class="token class-name"> </span><span class="token class-name punctuation">}</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">[</span><span class="token constant">K</span><span class="token plain"> </span><span class="token keyword">in</span><span class="token plain"> </span><span class="token keyword">keyof</span><span class="token plain"> </span><span class="token constant">T</span><span class="token plain"> </span><span class="token operator">&amp;</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation">]</span><span class="token operator">:</span><span class="token plain"> </span><span class="token constant">K</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token class-name maybe-class-name">IsValidTailwindClassString</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">K</span><span class="token class-name operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">?</span><span class="token plain"> </span><span class="token constant">T</span><span class="token punctuation">[</span><span class="token constant">K</span><span class="token punctuation">]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// If invalid class found, return the error</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">IsValidTailwindClassString</span><span class="token operator">&lt;</span><span class="token constant">K</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">The errors are correctly reported as we can see from the example.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token comment">// type Result1 = {</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">//   "p-1": true;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">//   flexx: "Error: 'x' is not a valid Tailwind class";</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// }</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Result1</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">TailwindClassNamesObject</span><span class="token operator">&lt;</span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token string">'p-1'</span><span class="token operator">:</span><span class="token plain"> </span><span class="token boolean">true</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token string">'flexx'</span><span class="token operator">:</span><span class="token plain"> </span><span class="token boolean">false</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token operator">&gt;</span><span class="token plain"></span></div></code></pre><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="7-array-type">7. Array type</h3><p class="max-w-xl font-sans mb-paragraph ">Finally we have the array format. Each item in the array is a string of class names that we need to validate.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">const</span><span class="token plain"> classNames </span><span class="token operator">=</span><span class="token plain"> </span><span class="token function">cn</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token string">'block'</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">'p-1 m-1'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// classNames = "block p-1 m-1"</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Turns out, you can manipulate the type of each individual array item using the same mapped type syntax as for objects. Let's use the same <code>MakeNullable</code> we used as an example in the object format to demonstrate (<a href="https://www.typescriptlang.org/play?#code/C4TwDgpgBAsghgawgOQK4Bt1wEbogHgBUA+KAXigG8AoKKAenqgCIAxAewCcoI4BjABZQkIKAAoA0gEooASwB2UdtgBWEPsHGEpzWlADaEuYpHsAZlEIBdAFyXDVqAB8o8jOmoBfatVCQoAEoQAM4YmhTwSGiYOHj4+m4AttgQnAA0UNjs7Hhw8lbEQA" class="cursor-pointer underline hover:underline ">TypeScript playground</a>).</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">MakeNullable</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token comment">// "For each index (K) in array (T)"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">[</span><span class="token constant">K</span><span class="token plain"> </span><span class="token keyword">in</span><span class="token plain"> </span><span class="token keyword">keyof</span><span class="token plain"> </span><span class="token constant">T</span><span class="token punctuation">]</span><span class="token operator">:</span><span class="token plain"> </span><span class="token constant">T</span><span class="token punctuation">[</span><span class="token constant">K</span><span class="token punctuation">]</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token keyword">null</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token comment">// type Result = [number | null, boolean | null]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Result</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">MakeNullable</span><span class="token operator">&lt;</span><span class="token punctuation">[</span><span class="token builtin">number</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token builtin">boolean</span><span class="token punctuation">]</span><span class="token operator">&gt;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Sometimes TypeScript is just magnificent. The syntax looks a bit odd, but one could argue that arrays do have object-like properties in JavaScript.</p><p class="max-w-xl font-sans mb-paragraph ">The final array type looks very similar to the object one, but with a few minor differences (<a href="https://www.typescriptlang.org/play?#code/C4TwDgpgBAwgNgQwM5IHIILbQLwCgpQA+UA5AEZwD2AxgNYn5EAFJAlgHZwcQC0FN9RsRIAzOBAAeDAsIDmAJ1YATaUVJlZPBQhA8AjAAYDq4WH2qA9BagA6O1ARRgAC0oBXJAnZKoGSvIhcXFBIKABJdiRgL2BWBFjKdgARCDAXACUIJTdqDlkAdWdWYAgkMARqHCFSAlJqkgITGqaSAB12BmDwaAAVRQwAZWj5YAAeHqhJEu8kKCjFdlkAPig8AgmpiBmoAAMAEgBvCKiYuITk1IysnLzC4tLyyoBfQ44RCHkoHoRWOCedxgEAD8X36QwQI3GPzgS0BUAAXF8giFev0AKLecaTCTTJSzeZ5FZrL7Y3GzfYHN4fKAABQCIlYEheR0i0XYsXirESKTSzky2VyizuJTKFQg-zhIL6rAwGKUozpEAZElhtQRSK6oWlGCxm22BMWRNBMrl4zBwzGPSWsM10AGYC4wB6lG+vwA7hwlPBkGhMKVdTitni5sAFstVlB7Y7na64B7vN6UOgsEgIuwPmaZeNrUErFAAKqxR0gJzdJyUKBgeSUABuymg7FYlU+H2rnxTnlkpVtUDR8nkowAsqVO9A9cGDeHsLs+23EYdhygEF2JT2o8UY9D417EEm-an2OmBxtA-rQ4TVowT2TdodE76sMyqZ9YxK1VLSUHya92O9PjBmVfAE1WBKAAG0YAAGlsOx1ydF0t09e9k39bUoV+a0AF04QIRFZ1GEgBlcNw4B8dhKGAKBnAQMBIA6G01TzABBJQlCySZ+38KivCULhFjhPMAAkePEWZqGQUooDdZwPmgHYeh2KAlEoSTyMojB4moZwBOsO9dwfcVDlfGwoAAMS4yRMAdQJGOscgqDoEhoJIJzSHadpXLYdgawQLgVGgrivJ8vyCH4RydJ0qBUAoiBEXk6FFOo2ZfICBAlBLMgIC2Jx+iwHwRGrDApKKEVHhs2pEWvL9b0pX9qSAnCoClaFP22ZC-Uagg83i35FIgABHNxfOSqBguUL5EO8KBqH0mxOqgPM5SgAJqDceQkC5dg5hySokBEEi4BAOaQNqEEwNjbCTtw40dVjFZx1mFz5q66wergRLkCgRJDuK+5RUqKBxBEYBjqul7eymla1o2xJtuoXb9rgQ7QbBs7LrBhbrAGSgsBcPJJjgJBoDdT6RHcbwUauvN0h+ImHC21t-Ge9UwPwnYSCM6EngaVhZjUhxRt88bY23ab9J2JZsMavMiPcUioHTGtqWo2jss++TFN5gXJ0p67Wf7AjZZIsiKO41X6KlvMAHEIGAWYXGgBl1soycvpEemHH7HQnGol2LVmD0XFIWd-ERBg80HDxKMyqAPHY76S2k7KNYcahgCGpGSw4Ga3DY5KGc4z57gwHsbeAUzWGdkPj1a4M3HYWhyLddgwMwokr1r2YwOfKBBIgNLoLsGwe4uyVe-7nwHpnQv51qv8oEXUc3xAiPNOcKAyfrpRoICdP5HYRqQT7tLpesGBElidg3GgImIS0-Gyc+LwOLbEMw0axEy4rqvC-QmE4URIrD4yIyxhCQAANSFkoEWSF9JDDDAGG8k524EFdlPHoY98IkHzOmCQkA07sS7EeJsb88gkFVLUVetAb5rTHBUdexcoBa0cGNHwOx2pYGAhVSMDoNwIXdLAn0KEkDZk7rAfSKFW6n3CO7EApRoJIB2iOemPhd5rS2g7UsoQU7FCJnAEQh8kQnTzGEd25Ed602gM-Rm8gP5QC-pXKI1dRhwU3AIhMEj9zZhtHmfMdMdheBAIpTK4k45ix9Owfcscib23kG4PG6YUDTRknQSCuA8w8SUipPmpsAiDUrtAAAVlHKAZBKCUHEF4JAPYYEeKEfuAA8mQQpEA06IOqgccC1CQCIknJhREgSoBPCNAcRgYEADSTCtrdMoO7CYAAyUhix+lQEmVPMBkC-K1J3D6eBeRRjjIoaBHoEz0bg1MVM1h4Skmb28DvW26ifZjkLgA8IECoHbPvHsxYBzYRPBAVqSaOy9wpiYl7EAojpyjIIBMqZUAZlzJWWs08wYNkfKBV888PyTnjMwlARZyCx44rOZjGRlyoHXNmLc7ey0Hn7yeS-JmapERoq2RiuBWLZDjFOfipZyxcBPAANxBB7JkRRcBKLTk+Z40F4LRhgRIGYPQnkwr0GchwVhJA264CAA" class="cursor-pointer underline hover:underline ">TypeScript playground</a>).</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">TailwindClassNamesArray</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">[</span><span class="token constant">K</span><span class="token plain"> </span><span class="token keyword">in</span><span class="token plain"> </span><span class="token keyword">keyof</span><span class="token plain"> </span><span class="token constant">T</span><span class="token punctuation">]</span><span class="token operator">:</span><span class="token plain"> </span><span class="token constant">K</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token class-name maybe-class-name">IsValidTailwindClassString</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">T</span><span class="token class-name punctuation">[</span><span class="token class-name constant">K</span><span class="token class-name punctuation">]</span><span class="token class-name"> </span><span class="token class-name operator">&amp;</span><span class="token class-name"> </span><span class="token class-name builtin">string</span><span class="token class-name operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">?</span><span class="token plain"> </span><span class="token constant">T</span><span class="token punctuation">[</span><span class="token constant">K</span><span class="token punctuation">]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// If invalid class found, return the error</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">IsValidTailwindClassString</span><span class="token operator">&lt;</span><span class="token constant">T</span><span class="token punctuation">[</span><span class="token constant">K</span><span class="token punctuation">]</span><span class="token plain"> </span><span class="token operator">&amp;</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Instead of validating just <code>K</code> <em class="italic">(the key in objects)</em>, we validate <code>T[K]</code> which refers to the array item itself. You could think the array as an object where the keys <em class="italic">(usually referred as indices)</em> are numeric starting from 0.</p><p class="max-w-xl font-sans mb-paragraph ">The <code>&amp; string</code> trick is required, but in a bit different context. The array item needs to "casted" into a string type, so that TypeScript is happy. Again, having <code>T extends string[]</code> constraint does not help here.</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="combining-everything">Combining everything</h2><p class="max-w-xl font-sans mb-paragraph ">With the individual pieces solved, it's time to put everything together.</p><p class="max-w-xl font-sans mb-paragraph ">Each parameter of the <code>classnames</code> function can be a string, object, or an array. To allow the same flexibility with strict types, we need a type utility that validates the input based on its shape. Conditional type it is.</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">TailwindClassParameterValue</span><span class="token class-name operator">&lt;</span><span class="token class-name constant">S</span><span class="token class-name operator">&gt;</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token constant">S</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token class-name builtin">string</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">?</span><span class="token plain"> </span><span class="token maybe-class-name">IsValidTailwindClassString</span><span class="token operator">&lt;</span><span class="token constant">S</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token operator">:</span><span class="token plain"> </span><span class="token constant">S</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token class-name builtin">any</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">?</span><span class="token plain"> </span><span class="token maybe-class-name">TailwindClassNamesArray</span><span class="token operator">&lt;</span><span class="token constant">S</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token operator">:</span><span class="token plain"> </span><span class="token constant">S</span><span class="token plain"> </span><span class="token keyword">extends</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token plain">key</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation">]</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">any</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token operator">?</span><span class="token plain"> </span><span class="token maybe-class-name">TailwindClassNamesObject</span><span class="token operator">&lt;</span><span class="token constant">S</span><span class="token operator">&gt;</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token operator">:</span><span class="token plain"> </span><span class="token comment">// Format not supported</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">        </span><span class="token builtin">never</span><span class="token punctuation">;</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">As you can see, nested ternaries are not that pleasant to read. Unfortunately it's all we got at type level.</p><p class="max-w-xl font-sans mb-paragraph ">Now, the last part. Typings for the <code>cn</code> function. Prepare to see a bit of an ugly trick..</p><pre class="py-0"><code class="language-typescript"><div class="token-line"><span class="token keyword">export</span><span class="token plain"> </span><span class="token keyword">function</span><span class="token plain"> </span><span class="token generic-function function">cn</span><span class="token generic-function generic class-name operator">&lt;</span><span class="token generic-function generic class-name constant">S1</span><span class="token generic-function generic class-name punctuation">,</span><span class="token generic-function generic class-name"> </span><span class="token generic-function generic class-name constant">S2</span><span class="token generic-function generic class-name punctuation">,</span><span class="token generic-function generic class-name"> </span><span class="token generic-function generic class-name constant">S3</span><span class="token generic-function generic class-name punctuation">,</span><span class="token generic-function generic class-name"> </span><span class="token generic-function generic class-name constant">S4</span><span class="token generic-function generic class-name punctuation">,</span><span class="token generic-function generic class-name"> </span><span class="token generic-function generic class-name constant">S5</span><span class="token generic-function generic class-name operator">&gt;</span><span class="token punctuation">(</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  c1</span><span class="token operator">?</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">TailwindClassParameterValue</span><span class="token operator">&lt;</span><span class="token constant">S1</span><span class="token operator">&gt;</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  c2</span><span class="token operator">?</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">TailwindClassParameterValue</span><span class="token operator">&lt;</span><span class="token constant">S2</span><span class="token operator">&gt;</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  c3</span><span class="token operator">?</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">TailwindClassParameterValue</span><span class="token operator">&lt;</span><span class="token constant">S3</span><span class="token operator">&gt;</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  c4</span><span class="token operator">?</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">TailwindClassParameterValue</span><span class="token operator">&lt;</span><span class="token constant">S4</span><span class="token operator">&gt;</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  c5</span><span class="token operator">?</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">TailwindClassParameterValue</span><span class="token operator">&lt;</span><span class="token constant">S5</span><span class="token operator">&gt;</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token comment">// If more than 5 class parameters are required, simply add new ones</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">)</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">return</span><span class="token plain"> </span><span class="token function">classnames</span><span class="token punctuation">(</span><span class="token plain">c1</span><span class="token punctuation">,</span><span class="token plain"> c2</span><span class="token punctuation">,</span><span class="token plain"> c3</span><span class="token punctuation">,</span><span class="token plain"> c4</span><span class="token punctuation">,</span><span class="token plain"> c5</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Voila! It's not pretty, but having the copy-pasted type parameters allows TypeScript to infer and validate the arguments individually.</p><p class="max-w-xl font-sans mb-paragraph ">That's the solution! I hope this was educational. Links to the final results:</p><ul class="pl-6 list-disc space-y-1"><li class="md:pl-2"><a href="https://replit.com/@KimmoBrunfeldt/the-ultimate-tailwind-typings#src/tailwindUtils.ts" class="cursor-pointer underline hover:underline ">Full working React project</a> in replit.com</li><li class="md:pl-2"><a href="https://www.typescriptlang.org/play?#code/C4TwDgpgBAwgNgQwM5IHIILbQLwCgpQA+UA5AEZwD2AxgNYn5EAFJAlgHZwcQC0FN9RsRIAzOBAAeDAsIDmAJ1YATaUVJlZPBQhA8AjAAYDq4WH2qA9BagA6O1ARRgAC0oBXJAnZKoGSvIhcXFBIKABJdiRgL2BWBFjKdgARCDAXACUIJTdqDlkAdWdWYAgkMARqHCFSAlJqkgITGqaSAB12BmDwaAAVRQwAZWj5YAAeHqhJEu8kKCjFdlkAPig8AgmpiBmoAAMAEgBvCKiYuITk1IysnLzC4tLyyoBfQ44RCHkoHoRWOCedxgEAD8X36QwQI3GPzgS0BUAAXF8giFev0AKLecaTCTTJSzeZ5FZrL7Y3GzfYHN4fKAABQCIlYEheR0i0XYsXirESKTSzky2VyizuJTKFQg-zhIL6rAwGKUozpEAZElhtQRSK6oWlGCxm22BMWRNBMrl4zBwzGPSWsM10AGYC4wB6lG+vwA7hwlPBkGhMKVdTitni5sAFstVlB7Y7na64B7vN6UOgsEgIuwPmaZeNrUErFAAKqxR0gJzdJyUKBgeSUABuymg7FYlU+H2rnxTnlkpVtUDR8nkowAsqVO9A9cGDeHsLs+23EYdhygEF2JT2o8UY9D417EEm-an2OmBxtA-rQ4TVowT2TdodE76sMyqZ9YxK1VLSUHya92O9PjBmVfAE1WBKAAG0YAAGlsOx1ydF0t09e9k39bUoV+a0AF04QIRFZ1GEgBlcNw4B8dhKGAKBnAQMBIA6G01TzABBJQlCySZ+38KivCULhFjhPMAAkePEWZqGQUooDdZwPmgHYeh2KAlEoSTyMojB4moZwBOsO9dwfcVDlfGwoAAMS4yRMAdQJGOscgqDoEhoJIJzSHadpXLYdgawQLgVGgrivJ8vyCH4RydJ0qBUAoiBEXk6FFOo2ZfICBAlBLMgIC2Jx+iwHwRGrDApKKEVHhs2pEWvL9b0pX9qSAnCoClaFP22ZC-Uagg83i35FIgABHNxfOSqBguUL5EO8KBqH0mxOqgPM5SgAJqDceQkC5dg5hySokBEEi4BAOaQNqEEwNjbCTtw40dVjFZx1mFz5q66wergRLkCgRJDuK+5RUqKBxBEYBjqul7eymla1o2xJtuoXb9rgQ7QbBs7LrBhbrAGSgsBcPJJjgJBoDdT6RHcbwUauvN0h+ImHC21t-Ge9UwPwnYSCM6EngaVhZjUhxRt88bY23ab9J2JZsMavMiPcUioHTGtqWo2jss++TFN5gXJ0p67Wf7AjZZIsiKO41X6KlvMAHEIGAWYXGgBl1soycvpEemHH7HQnGol2LVmD0XFIWd-ERBg80HDxKMyqAPHY76S2k7KNYcahgCGpGSw4Ga3DY5KGc4z57gwHsbeAUzWGdkPj1a4M3HYWhyLddgwMwokr1r2YwOfKBBIgNLoLsGwe4uyVe-7nwHpnQv51qv8oEXUc3xAiPNOcKAyfrpRoICdP5HYRqQT7tLpesGBElidg3GgImIS0-Gyc+LwOLbEMw0axEy4rqvC-QmE4URIrD4yIyxhCQAANSFkoEWSF9JDDDAGG8k524EFdlPHoY98IkHzOmCQkA07sS7EeJsb88gkFVLUVetAb5rTHBUdexcoBa0cGNHwOx2pYGAhVSMDoNwIXdLAn0KEkDZk7rAfSKFW6n3CO7EApRoJIB2iOemPhd5rS2g7UsoQU7FCJnAEQh8kQnTzGEd25Ed602gM-Rm8gP5QC-pXKI1dRhwU3AIhMEj9zZhtHmfMdMdheBAIpTK4k45ix9Owfcscib23kG4PG6YUDTRknQSCuA8w8SUipPmpsAiDUrtAAAVlHKAZBKCUHEF4JAPYYEeKEfuAA8mQQpEA06IOqgccC1CQCIknJhREgSoBPCNAcRgYEADSTCtrdMoO7CYAAyUhix+lQEmVPMBkC-K1J3D6eBeRRjjIoaBHoEz0bg1MVM1h4Skmb28DvW26ifZjkLgA8IECoHbPvHsxYBzYRPBAVqSaOy9wpiYl7EAojpyjIIBMqZUAZlzJWWs08wYNkfKBV888PyTnjMwlARZyCx44rOZjGRlyoHXNmLc7ey0Hn7yeS-JmapERoq2RiuBWLZDjFOfipZyxcBPAANy5msDSCEfoSifHEltGOjhJzQQhPIb2XFKDNNaZRR+GlgC4EkGAfwlEUQTXccCpAYqlW4w+Js6+LijQDDEZORgIJWXKE+RyhBAwKGIjtVPQJUjjnsvqaC8FNrGBerEZ0sC3TemcpWYM-5-rjUcNKE0lpbSPWhtJeZeQWqFam0UbRfVWQ4RAPkMKkVUB8hKtVp8R+pAZoRP3A0Pw2RxBSWKOvAkactF5HSdYSOcBYjWS0dAco5rbYfGSgEOYFYXDxC+Ha5hFBoDAArM+LYAN+5aUrOKi1RdvCsDrNkXyh0dV4P1RveuadNrTXYC4vQ0EBgACYH0AGYH0ABYH0AFYlgAApGDUD0ECSqAaUBmolZa3y1qBh6CWGkgg1BH3AaNXGQRYGd3jvkFaiALjH1wYAy+5DrqfTgd3dhlxL78MIffUR0DpqMOSvIwMd9VHppfto4m-SpHMNMZ-fB0lFy-BTtnVtL9lLt1jslZO6AeS3AFJpRtDADoSxpTIhAN0X1Em4AAJTRrDFAaFpKegNKSA0xE4kkZ1v0pElMTbKAtvKmo+lT1-m4GoOwX9JAzB6BINptzHn7ICFcmBLz5hnLedKQ5eg0EuEEAUMoOEGgtBKt0IYAwuAdiYT8+539hnQs+cRKGa+Aq-NBBy-lzyHBWG+f855sK0XwIVeclVqBJAsu1byy1vyJBCtxMCE8bTQA" class="cursor-pointer underline hover:underline ">TypeScript playground</a></li></ul><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="credits--further-reading">Credits &amp; further reading</h2><p class="max-w-xl font-sans mb-paragraph ">The final types took quite a while to finish, and I couldn't have done it without help. Credits to <a href="https://github.com/ahejlsberg" class="cursor-pointer underline hover:underline ">@ahejlsberg</a>, <a href="https://twitter.com/virtualkirill" class="cursor-pointer underline hover:underline ">@virtualkirill</a>, and <a href="https://stackoverflow.com/users/2887218/jcalz" class="cursor-pointer underline hover:underline ">@jcalz</a> for terrific online resources.</p><ul class="pl-6 list-disc space-y-1"><li class="md:pl-2"><a href="https://dev.to/virtualkirill/make-your-css-safer-by-type-checking-tailwind-css-classes-2l14" class="cursor-pointer underline hover:underline ">Make your CSS safer by type checking Tailwind CSS classes</a></li><li class="md:pl-2"><a href="https://stackoverflow.com/questions/65737948/how-to-type-check-if-object-keys-conform-a-conditional-recursive-template-type/65738172#65738172" class="cursor-pointer underline hover:underline ">How to type check if object keys conform a conditional recursive template type?
</a></li><li class="md:pl-2"><a href="https://github.com/microsoft/TypeScript/pull/40336" class="cursor-pointer underline hover:underline ">Pull request</a> that introduced template literal types</li><li class="md:pl-2"><a href="https://github.com/ghoullier/awesome-template-literal-types" class="cursor-pointer underline hover:underline ">awesome-template-literal-types</a> — Collection of even more resources</li><li class="md:pl-2"><a href="https://github.com/g-plane/type-gymnastics" class="cursor-pointer underline hover:underline ">Type Gymnastics</a> — Collection of wonderful TypeScript type gymnastics code snippets</li><li class="md:pl-2"><a href="https://github.com/codemix/ts-sql" class="cursor-pointer underline hover:underline ">ts-sql</a> — SQL "database" implemented purely in TypeScript.</li><li class="md:pl-2"><a href="https://github.com/0kku/destiny/blob/a3d5182658599bc117db3b4657fed71dfedf8b44/src/parsing/XmlTypeParser.ts" class="cursor-pointer underline hover:underline ">XML parser in TypeScript</a></li><li class="md:pl-2"><a href="https://github.com/gmono/ts-metacode" class="cursor-pointer underline hover:underline ">ts-metacode</a> — TypeScript metaprogramming</li></ul><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="utilities">Utilities</h3><p class="max-w-xl font-sans mb-paragraph ">We ended up implementing numerous advanced TypeScript tricks from scratch.
It might make sense to use an existing library instead. There are lodash-like utility packages for TypeScript types too:</p><ul class="pl-6 list-disc space-y-1"><li class="md:pl-2"><a href="https://github.com/sindresorhus/type-fest" class="cursor-pointer underline hover:underline ">type-fest</a> — A collection of essential TypeScript types</li><li class="md:pl-2"><a href="https://github.com/millsp/ts-toolbelt" class="cursor-pointer underline hover:underline ">ts-toolbelt</a> — TypeScript's largest utility library</li><li class="md:pl-2"><a href="https://github.com/krzkaczor/ts-essentials" class="cursor-pointer underline hover:underline ">ts-essentials</a> — All basic TypeScript types in one place</li><li class="md:pl-2"><a href="https://github.com/piotrwitek/utility-types" class="cursor-pointer underline hover:underline ">utility-types</a> — Think "lodash" for static types</li></ul><p class="max-w-xl font-sans mb-paragraph ">Call to action here. Just kidding, I'm doing this for fun. For now..</p></div></body></html>]]></content:encoded><content:media url="https://kimmo.blog/content-assets/6/cover.png" type="image/jpeg" medium="image" xmlns:media="http://search.yahoo.com/mrss/"/></item><item><title><![CDATA[Building an e-ink weather display for our home]]></title><description><![CDATA[Building a battery powered e-ink weather forecast display for our home. This was a fun Raspberry Pi project.
]]></description><link>https://kimmo.blog/posts/7-building-eink-weather-display-for-our-home/</link><guid isPermaLink="false">https://kimmo.blog/posts/7-building-eink-weather-display-for-our-home/</guid><category><![CDATA[eink]]></category><category><![CDATA[raspberrypi]]></category><category><![CDATA[hardware]]></category><pubDate>Sun, 13 Nov 2022 00:00:00 GMT</pubDate><enclosure url="https://kimmo.blog/content-assets/7/cover.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<html><head></head><body><div data-reactroot=""><p class="max-w-xl font-sans mb-paragraph "><em class="italic">This post describes the build-process. You can find more about the software in the <a href="https://github.com/kimmobrunfeldt/eink-weather-display" class="cursor-pointer underline hover:underline ">GitHub repo</a>.</em></p><div class="my-4 mb-12"><a href="https://kimmo.blog/content-assets/7/scene.jpg" rel="noreferrer" target="_blank"><img class="mx-auto w-full" alt="The final build" src="https://kimmo.blog/content-assets/7/scene.jpg"></a><p class="italic text-sm mt-4 text-center">The final build</p></div><p class="max-w-xl font-sans mb-paragraph ">We almost always want to know today's weather before leaving the apartment. Often the whole event is quite a hassle: getting our daughter dressed, finding all our personal outwear, plus trying to find your phone to check the weather. To make the experience 1% better, the solution obviously needs engineering.</p><p class="max-w-xl font-sans mb-paragraph ">I decided to build a DIY battery powered e-ink weather display for our home. After researching a while, I ended up with a few goals for the build:</p><ul class="pl-6 list-disc space-y-1"><li class="md:pl-2">Easily glanceable weather forecast at the heart of our home. Ideally eliminates one more reason to pick up the phone.</li><li class="md:pl-2">Looks like a "real product". The housing should look professional.</li><li class="md:pl-2">Fully battery-powered. We didn't want a visible cable, and drilling the cable inside wall wasn't an option.</li><li class="md:pl-2">Always visible and doesn't light up the hallway during evening / night. This leads to e-ink display.</li><li class="md:pl-2">Primarily for our use case, but with reusability in mind. For example custom location and timezone. The end result is somewhat tied to Finland due to using the API of Finnish meteorological institute, but nevertheless the design goal was kept in mind.</li></ul><p class="max-w-xl font-sans mb-paragraph ">The goals come with a few challenges:</p><ul class="pl-6 list-disc space-y-1"><li class="md:pl-2">Battery life obviously. Fortunately e-ink displays have an interesting property: the rendered image stays visible as is even if all cables are disconnected. It doesn't require any power to keep the image.</li><li class="md:pl-2">Low refresh speed due to battery life constraint. My plan was to refresh the screen only once or twice a day. That brings interesting design challenges.  How do you indicate that the data is not real time? What should we show as the day's temperature: average or maximum for the day?</li><li class="md:pl-2">Physical constraints by the frame. Ideally it would be flush to the wall behind.</li></ul><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="the-plan">The plan</h2><p class="max-w-xl font-sans mb-paragraph ">The device should wake up early in the morning, fetch the latest weather forecast information, update the information to the e-ink display, and go back to deep sleep until tomorrow.</p><p class="max-w-xl font-sans mb-paragraph ">That way, we would have a fresh weather forecast for the day while consuming minimal amount of battery.</p><p class="max-w-xl font-sans mb-paragraph ">Then comes the execution.</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="execution">Execution</h2><p class="max-w-xl font-sans mb-paragraph ">First I designed the weather UI in Figma. This took around 2 days (i.e. evenings after work).</p><p class="max-w-xl font-sans mb-paragraph "><a href="https://kimmo.blog/content-assets/7/figma.png" rel="noreferrer" target="_blank"><img src="https://kimmo.blog/content-assets/7/figma.png" class="my-12"></a></p><p class="max-w-xl font-sans mb-paragraph ">The designs were intended for the <a href="https://www.waveshare.com/7.5inch-e-paper-hat.htm" class="cursor-pointer underline hover:underline ">Waveshare 7.5" e-ink display</a>. After ordering one, I started looking into good fonts to display in a 1-bit (black and white) screen. It was surprisingly hard to find real 1-bit-screen fonts, so I tested a few sample renders using regular web fonts with and without post-processing:</p><p class="max-w-xl font-sans mb-paragraph "><a href="https://kimmo.blog/content-assets/7/test-anti-alias-disabled.png" rel="noreferrer" target="_blank"><img src="https://kimmo.blog/content-assets/7/test-anti-alias-disabled.png" class="my-12"></a>
<a href="https://kimmo.blog/content-assets/7/test-post-process-all-pixels-black.png" rel="noreferrer" target="_blank"><img src="https://kimmo.blog/content-assets/7/test-post-process-all-pixels-black.png" class="my-12"></a>
<a href="https://kimmo.blog/content-assets/7/test-post-process-threshold-pixels-black.png" rel="noreferrer" target="_blank"><img src="https://kimmo.blog/content-assets/7/test-post-process-threshold-pixels-black.png" class="my-12"></a></p><p class="max-w-xl font-sans mb-paragraph ">I wasn't super satisfied.. the product would be in a central place in our apartment, so it should look nice.</p><p class="max-w-xl font-sans mb-paragraph ">So... after thinking a bit, I ordered the 10.3" 16-gray-scale (4 bits) display in addition, since it allows text to be anti-aliased. The end result is way smoother. It would've been possible to just live with the 1-bit display constraint, but I decided to use that screen for something else in the future.</p><p class="max-w-xl font-sans mb-paragraph ">On the same day, I also ordered a bunch of other hardware I was expecting to use: SD card, PiJuice chip and battery, standoff screws, GPIO cables and adapters. Fortunately a friend of mine had a spare Raspberry Pi Zero lying around which I could use.</p><p class="max-w-xl font-sans mb-paragraph ">After the Figma designs were ready enough, I started creating the HTML page which is used to render the UI. Many existing projects used lower level libraries such as the Python Imaging Library, but I opted for HTML and CSS. Development is fast when you can edit the HTML and immediately see the result without rendering a raster image after each iteration.</p><p class="max-w-xl font-sans mb-paragraph ">UI development took rougly 1-2 weeks when coding in the evenings and weekends. Having the flu delayed the build a bit unfortunately.</p><p class="max-w-xl font-sans mb-paragraph "><a href="https://kimmo.blog/content-assets/7/web-app-dev.jpg" rel="noreferrer" target="_blank"><img src="https://kimmo.blog/content-assets/7/web-app-dev.jpg" class="my-12"></a></p><p class="max-w-xl font-sans mb-paragraph ">The software vs hardware timing realised like a perfect Gantt chart. As soon as I was done fine-tuning the UI, the hardware arrived in Finland. Nice.</p><p class="max-w-xl font-sans mb-paragraph ">Even though the development was done with the 1872x1404 display resolution in mind, I could finally see how it actually looked. The Raspberry Pi installation and setup took quite some time, but it paid off. It was absolutely amazing to see the UI appear the first time on the e-ink display.</p><div class=" my-12"><a href="https://kimmo.blog/content-assets/7/first-display.jpg" rel="noreferrer" target="_blank"><img class="mx-auto w-full" alt="First display" src="https://kimmo.blog/content-assets/7/first-display.jpg"></a><p class="italic text-sm mt-4 text-center">Past bedtime</p></div><p class="max-w-xl font-sans mb-paragraph ">Everything was surprisingly smooth sailing to this point. But then I assembled all the parts together and realized how small the frame actually is.</p><p class="max-w-xl font-sans mb-paragraph "><a href="https://kimmo.blog/content-assets/7/gpio-too-tall.jpg" rel="noreferrer" target="_blank"><img src="https://kimmo.blog/content-assets/7/gpio-too-tall.jpg" class="my-12"></a></p><p class="max-w-xl font-sans mb-paragraph ">I didn't want to build a larger visible casing to the back, so it was time to go back to the drawing board. I tried to fit everything in the frame by testing all kinds of combinations. This monstrous GPIO cable + 1-to-2 adapter setup almost did the job:</p><p class="max-w-xl font-sans mb-paragraph "><a href="https://kimmo.blog/content-assets/7/gpio-cable.jpg" rel="noreferrer" target="_blank"><img src="https://kimmo.blog/content-assets/7/gpio-cable.jpg" class="my-12"></a></p><p class="max-w-xl font-sans mb-paragraph ">But even after cutting all extra plastic pieces from the cables, it wasn't possible to mount the frame flush to the wall.</p><p class="max-w-xl font-sans mb-paragraph ">Then I realised that the Waveshare demo page featured a Windows app that connected to the controller via the micro-USB interface. Would it be possible to use that to control the display? There weren't any examples of doing that in a Linux box though.</p><p class="max-w-xl font-sans mb-paragraph ">Fortunately for me, some great minds had already <a href="https://twitter.com/faassen/status/1375922965062238208?lang=en" class="cursor-pointer underline hover:underline ">thought the same</a>. I connected the controller using USB cables, built the C code in the Raspberry Pi, and tried to clear the display as a test. It worked! What a relief. USB cables are <em class="italic">so much</em> smaller than the 40-pin GPIO cables and headers.</p><p class="max-w-xl font-sans mb-paragraph ">Now that I was confident about the USB-strategy, I needed to get rid of the GPIO header of the IT851 controller. The chip was still too tall for the frame with it attached. Fortunately, Helsinki has an amazing library <em class="italic">(more like a library, hacker space, gaming cafe, and a public hangout place combined)</em> where I could go and desolder the header <em class="italic">for free</em>.</p><div class=" my-12"><a href="https://kimmo.blog/content-assets/7/oodi.jpg" rel="noreferrer" target="_blank"><img class="mx-auto w-full" alt="Oodi" src="https://kimmo.blog/content-assets/7/oodi.jpg"></a><p class="italic text-sm mt-4 text-center">The picture is from the library's electronics room which has cool gear such as laser cutter, 3D printer, and much more.</p></div><p class="max-w-xl font-sans mb-paragraph ">It's mind-boggling how cool Oodi is architecturally but also functionally.</p><p class="max-w-xl font-sans mb-paragraph ">Unfortunately, my soldering skills weren't as mind-boggling. The estimate that I gave to my wife was that the job would take around 20 minutes. But even after 1.5 hours of desoldering and googling how to desolder components, I wasn't able to rip the header off the chip. My time was up in the soldering lab.</p><p class="max-w-xl font-sans mb-paragraph ">Next strategy was brute force. I took my side cutters and just chopped half of the header off. All I could hope for was that the chip would work after the treatment. With the GPIO header chopped in half, all parts finally fit the IKEA frame nicely!</p><p class="max-w-xl font-sans mb-paragraph ">The unknowns in the software had been solved, and the final assembly could begin. I mounted everything to the IKEA frame using screws and hot glue. The display is super thin, so I avoided placing any screws on the back cover directly behind the display. The back cover bends slightly so the screws could end up damaging the panel while moving the product for charging.</p><p class="max-w-xl font-sans mb-paragraph "><a href="https://kimmo.blog/content-assets/7/final-build-back.jpg" rel="noreferrer" target="_blank"><img src="https://kimmo.blog/content-assets/7/final-build-back.jpg" class="my-12"></a></p><p class="max-w-xl font-sans mb-paragraph ">Tada! Components were able to fit almost perfectly within the 2.9cm thick IKEA Frame. Out of the 2.9cm of thickness, approximately 2cm was left for the actual components.</p><p class="max-w-xl font-sans mb-paragraph "><a href="https://kimmo.blog/content-assets/7/thickness.jpg" rel="noreferrer" target="_blank"><img src="https://kimmo.blog/content-assets/7/thickness.jpg" class="my-12"></a></p><p class="max-w-xl font-sans mb-paragraph ">I was super worried about component damage after the desoldering amateur hour, but fortunately everything still worked.</p><p class="max-w-xl font-sans mb-paragraph ">Finally, I tested a few error cases.</p><p class="max-w-xl font-sans mb-paragraph "><a href="https://kimmo.blog/content-assets/7/error-example.jpg" rel="noreferrer" target="_blank"><img src="https://kimmo.blog/content-assets/7/error-example.jpg" class="my-12"></a></p><p class="max-w-xl font-sans mb-paragraph "><a href="https://kimmo.blog/content-assets/7/battery-empty.jpg" rel="noreferrer" target="_blank"><img src="https://kimmo.blog/content-assets/7/battery-empty.jpg" class="my-12"></a></p><p class="max-w-xl font-sans mb-paragraph ">Those done, it was time to mount the frame on our wall.</p><p class="max-w-xl font-sans mb-paragraph "><a href="https://kimmo.blog/content-assets/7/sunset-product.jpg" rel="noreferrer" target="_blank"><img src="https://kimmo.blog/content-assets/7/sunset-product.jpg" class="my-12"></a></p><p class="max-w-xl font-sans mb-paragraph "><a href="https://kimmo.blog/content-assets/7/closeup.jpg" rel="noreferrer" target="_blank"><img src="https://kimmo.blog/content-assets/7/closeup.jpg" class="my-12"></a></p><p class="max-w-xl font-sans mb-paragraph "><a href="https://kimmo.blog/content-assets/7/scene.jpg" rel="noreferrer" target="_blank"><img src="https://kimmo.blog/content-assets/7/scene.jpg" class="my-12"></a></p><p class="max-w-xl font-sans mb-paragraph ">I really like the end result. The IKEA frame has a white cardboard border, which we ended up cutting to fit the screen. To not have mismatching aspect ratios of the white cardboard border and the frame itself, I sacrificed a few vertical pixels of the e-ink display. The white cover board opening is vertically shorter than what the e-ink display is capable of, leaving some Y-pixels unused. A bit of a shame, but the overall look is most important!</p><p class="max-w-xl font-sans mb-paragraph ">One option I considered was to saw the frame to be a perfect fit for the screen. However, it could've lead to a messy end result and it was much easier to mount everything to the back cover with the extra space.</p><p class="max-w-xl font-sans mb-paragraph ">The project took 3 weeks in calendar time. I was positively surprised about the quick turnaround time. It didn't happen by chance though. I purposely chose familiar hardware and software to make the development easier. The idea was to actually finish the project, even though lower-level tinkering would've been educational and beneficial.</p><p class="max-w-xl font-sans mb-paragraph ">That's everything, it was fun!</p><h3 class="text-xl mt-9 mb-3 font-heading font-black text-gray-9 dark:text-gray-3" id="hardware-list-for-reference">Hardware list for reference</h3><ul class="pl-6 list-disc space-y-1"><li class="md:pl-2">Raspberry Pi Zero W</li><li class="md:pl-2"><a href="https://uk.pi-supply.com/products/pijuice-zero" class="cursor-pointer underline hover:underline ">PiJuice Zero</a></li><li class="md:pl-2"><a href="https://uk.pi-supply.com/products/pijuice-12000mah-battery" class="cursor-pointer underline hover:underline ">PiJuice 12000mAh battery</a>. As large as possible to avoid having to charge the device often.</li><li class="md:pl-2"><a href="https://www.waveshare.com/10.3inch-e-paper-hat.htm" class="cursor-pointer underline hover:underline ">Waveshare 10.3" 1872x1404 e-ink display with Raspberry Pi HAT</a>. Supports 16 shades of black and white.</li><li class="md:pl-2"><a href="https://www.amazon.de/-/en/gp/product/B07MN2GY6Y/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&amp;psc=1" class="cursor-pointer underline hover:underline ">Geekworm Raspberry Pi Installation Tool 132 Pcs</a>. For a set of spacers and screws that fit Raspberry Pi projects nicely.</li><li class="md:pl-2">Micro-usb to USB adapter</li><li class="md:pl-2">USB to micro-usb cable</li><li class="md:pl-2"><a href="https://www.ikea.com/fi/fi/p/hovsta-kehys-ja-kehyskartonki-koivukuvio-40365762/" class="cursor-pointer underline hover:underline ">IKEA Hovsta Frame</a></li><li class="md:pl-2">Misc building items: hot glue, hair band to hold the battery, wall mounting hooks, small plastic box cut to pieces to support the battery from below, and of course duct tape.</li></ul><p class="max-w-xl font-sans mb-paragraph ">Hardware bought but not needed in the end:</p><ul class="pl-6 list-disc space-y-1"><li class="md:pl-2"><a href="https://www.amazon.de/-/en/gp/product/B08C4S8NPH/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&amp;psc=1" class="cursor-pointer underline hover:underline ">GeeekPi Micro Connectors Raspberry Pi 40-pin GPIO 1 to 2 Expansion Board</a>. To connect PiJuice and e-ink display nicely.</li><li class="md:pl-2"><a href="https://www.amazon.de/-/en/gp/product/B08VRJ51T4/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&amp;psc=1" class="cursor-pointer underline hover:underline ">GPIO Cable for Raspberry Pi 40 Pin</a>. To allow a bit more flexibility inside the build.</li></ul></div></body></html>]]></content:encoded><content:media url="https://kimmo.blog/content-assets/7/cover.png" type="image/jpeg" medium="image" xmlns:media="http://search.yahoo.com/mrss/"/></item><item><title><![CDATA[AST-based refactoring with ts-morph]]></title><description><![CDATA[Mass-refactoring TypeScript code using ts-morph. The AST-based code transformation makes it quick to refactor even the largest codebases.
]]></description><link>https://kimmo.blog/posts/8-ast-based-refactoring-with-ts-morph/</link><guid isPermaLink="false">https://kimmo.blog/posts/8-ast-based-refactoring-with-ts-morph/</guid><category><![CDATA[typescript]]></category><category><![CDATA[ast]]></category><category><![CDATA[ts-morph]]></category><category><![CDATA[refactoring]]></category><pubDate>Wed, 19 Apr 2023 00:00:00 GMT</pubDate><enclosure url="https://kimmo.blog/content-assets/8/cover.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<html><head></head><body><div data-reactroot=""><p class="max-w-xl font-sans mb-paragraph ">I had a refactoring problem. My increasingly large codebase had widespread database entity model code that used the following signature:</p><pre class="py-0"><code class="language-ts"><div class="token-line"><span class="token keyword">await</span><span class="token plain"> </span><span class="token maybe-class-name">User</span><span class="token punctuation">.</span><span class="token method function property-access">findMany</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token plain"> name</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">'Lisa'</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Which isn't bad per se, but the property names of a model are sharing the same object key namespace with other relevant parameters I was introducing. Relevant parameters such as <code>orderBy</code> and <code>connection</code> object:</p><pre class="py-0"><code class="language-ts"><div class="token-line"><span class="token keyword">await</span><span class="token plain"> </span><span class="token maybe-class-name">User</span><span class="token punctuation">.</span><span class="token method function property-access">findMany</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  name</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">'Lisa'</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  orderBy</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token string">'createdAt'</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">'desc'</span><span class="token punctuation">]</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  connection</span><span class="token operator">:</span><span class="token plain"> transaction</span><span class="token punctuation">,</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">I lived with the original approach until it bothered me enough to make a move. Let's go through the process together.</p><p class="max-w-xl font-sans mb-paragraph ">The plan was to refactor all calls to the following form:</p><pre class="py-0"><code class="language-ts"><div class="token-line"><span class="token keyword">await</span><span class="token plain"> </span><span class="token maybe-class-name">User</span><span class="token punctuation">.</span><span class="token method function property-access">findMany</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token plain"> where</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"> name</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string">'Lisa'</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">More verbose but also more explicit. In the new format, the unknown property names of models would be clearly isolated from the pre-defined parameters within a <code>where</code> wrapper. Anyways, we're not here to judge my refactoring decisions but to learn how to undo the bad ones quickly using automation.</p><p class="max-w-xl font-sans mb-paragraph ">The problem was that there were <strong class="font-bold">119 call sites</strong>. Not <em class="italic">that</em> many – but enough to consider if spending an evening for a manual search &amp; replace operation is worth the trouble. Of course not!</p><p class="max-w-xl font-sans mb-paragraph ">I also wanted to level up my AST-based tooling skills so this would be a great opportunity.</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="the-pieces">The pieces</h2><p class="max-w-xl font-sans mb-paragraph ">I decided to use <a href="https://ts-morph.com/" class="cursor-pointer underline hover:underline ">ts-morph</a>. It provides a convenient API to access, traverse, and modify TypeScript code via AST operations. The basic refactoring formula goes like this:</p><pre class="py-0"><code class="language-ts"><div class="token-line"><span class="token keyword">import</span><span class="token plain"> </span><span class="token imports punctuation">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">Project</span><span class="token imports punctuation">,</span><span class="token imports"> </span><span class="token imports maybe-class-name">SyntaxKind</span><span class="token imports"> </span><span class="token imports punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">'ts-morph'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> project </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword">new</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Project</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">project</span><span class="token punctuation">.</span><span class="token method function property-access">addSourceFilesAtPaths</span><span class="token punctuation">(</span><span class="token string">'src/**/*.ts'</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">const</span><span class="token plain"> sourceFiles </span><span class="token operator">=</span><span class="token plain"> project</span><span class="token punctuation">.</span><span class="token method function property-access">getSourceFiles</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">for</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token keyword">const</span><span class="token plain"> file </span><span class="token keyword">of</span><span class="token plain"> sourceFiles</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token comment">// TODO: traverse and modify file AST as needed</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">await</span><span class="token plain"> file</span><span class="token punctuation">.</span><span class="token method function property-access">save</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token comment">// Save file changes to disk</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Next we need to find our database model call syntax nodes and learn how to modify their code.</p><p class="max-w-xl font-sans mb-paragraph ">To get started, I usually copy paste a minimal example code to <a href="https://ts-ast-viewer.com/" class="cursor-pointer underline hover:underline ">https://ts-ast-viewer.com/</a>. This makes it easier to understand what to look for. For example a simple <code>await User.findMany({ name: 'Lisa' })</code> becomes:</p><pre class="py-0"><code class="language-no-highlight"><div class="token-line"><span class="token plain">SourceFile</span></div><div class="token-line"><span class="token plain">  ExpressionStatement</span></div><div class="token-line"><span class="token plain">    AwaitExpression</span></div><div class="token-line"><span class="token plain">      CallExpression</span></div><div class="token-line"><span class="token plain">        PropertyAccessExpression</span></div><div class="token-line"><span class="token plain">          Identifier</span></div><div class="token-line"><span class="token plain">          Identifier</span></div><div class="token-line"><span class="token plain">        ObjectLiteralExpression</span></div><div class="token-line"><span class="token plain">          PropertyAssignment</span></div><div class="token-line"><span class="token plain">            Identifier</span></div><div class="token-line"><span class="token plain">            StringLiteral</span></div><div class="token-line"><span class="token plain">  EndOfFileToken</span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Pasting too much code makes the tree difficult to understand as the AST is so verbose.</p><p class="max-w-xl font-sans mb-paragraph ">Once you have an understanding of the nodes types to look for, jump back to the editor. We can use the <code>file.getDescendantsOfKind()</code> method and simply log everything we find. Let's start with <code>AwaitExpression</code>s.</p><pre class="py-0"><code class="language-ts"><div class="token-line"><span class="token keyword">for</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token keyword">const</span><span class="token plain"> file </span><span class="token keyword">of</span><span class="token plain"> sourceFiles</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">const</span><span class="token plain"> awaitExpressions </span><span class="token operator">=</span><span class="token plain"> file</span><span class="token punctuation">.</span><span class="token method function property-access">getDescendantsOfKind</span><span class="token punctuation">(</span><span class="token maybe-class-name">SyntaxKind</span><span class="token punctuation">.</span><span class="token property-access maybe-class-name">AwaitExpression</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">for</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token keyword">const</span><span class="token plain"> awaitExpression </span><span class="token keyword">of</span><span class="token plain"> awaitExpressions</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token plain">awaitExpression</span><span class="token punctuation">.</span><span class="token method function property-access">getText</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">This should print out all of them. However we're only interested of the database entity methods, so let's narrow it down further by checking if a descendant <code>PropertyAccessExpression</code> can be found with a specific name.</p><pre class="py-0"><code class="language-ts"><div class="token-line"><span class="token keyword">function</span><span class="token plain"> </span><span class="token function">shouldRefactor</span><span class="token punctuation">(</span><span class="token plain">awaitExpression</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">AwaitExpression</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">const</span><span class="token plain"> paExpression </span><span class="token operator">=</span><span class="token plain"> awaitExpression</span><span class="token punctuation">.</span><span class="token method function property-access">getFirstDescendantByKind</span><span class="token punctuation">(</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token maybe-class-name">SyntaxKind</span><span class="token punctuation">.</span><span class="token property-access maybe-class-name">PropertyAccessExpression</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">if</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token operator">!</span><span class="token plain">paExpression </span><span class="token operator">||</span><span class="token plain"> paExpression</span><span class="token punctuation">.</span><span class="token method function property-access">wasForgotten</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// In certain cases ts-morph stops tracking nodes for performance</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// optimisation reasons. If the node was forgotten, accessing it throws.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token keyword">return</span><span class="token plain"> </span><span class="token boolean">false</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">const</span><span class="token plain"> name </span><span class="token operator">=</span><span class="token plain"> paExpression</span><span class="token punctuation">.</span><span class="token method function property-access">getName</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">const</span><span class="token plain"> entityMethods </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token string">'find'</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">'maybeFind'</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">'findMany'</span><span class="token punctuation">]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">return</span><span class="token plain"> entityMethods</span><span class="token punctuation">.</span><span class="token method function property-access">includes</span><span class="token punctuation">(</span><span class="token plain">name</span><span class="token operator">!</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Finally we need to modify the await expression as required. Luckily for me none of the call sites used any other parameters than the object property names. That allows us to simply just wrap the original object parameter with a new one: <code>{ where: &lt;original&gt; }</code>.</p><p class="max-w-xl font-sans mb-paragraph ">To do that we can use the <code>getText()</code> method of <code>ObjectLiteralExpression</code> node to get the original <code>{ name: 'Lisa' }</code> parameter as text and replace it with the new format using <code>replaceWithText()</code>.</p><pre class="py-0"><code class="language-ts"><div class="token-line"><span class="token keyword">function</span><span class="token plain"> </span><span class="token function">refactor</span><span class="token punctuation">(</span><span class="token plain">awaitExpression</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">AwaitExpression</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">const</span><span class="token plain"> param </span><span class="token operator">=</span><span class="token plain"> awaitExpression</span><span class="token punctuation">.</span><span class="token method function property-access">getFirstDescendantByKind</span><span class="token punctuation">(</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token maybe-class-name">SyntaxKind</span><span class="token punctuation">.</span><span class="token property-access maybe-class-name">ObjectLiteralExpression</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">if</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token operator">!</span><span class="token plain">param</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token keyword">throw</span><span class="token plain"> </span><span class="token keyword">new</span><span class="token plain"> </span><span class="token class-name known-class-name class-name">Error</span><span class="token punctuation">(</span><span class="token template-string template-punctuation string">`</span><span class="token template-string string">SyntaxKind.ObjectLiteralExpression not found</span><span class="token template-string template-punctuation string">`</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">const</span><span class="token plain"> paramAsTxt </span><span class="token operator">=</span><span class="token plain"> param</span><span class="token punctuation">.</span><span class="token method function property-access">getText</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token comment">// e.g. "{ name: 'Lisa' }"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  param</span><span class="token punctuation">.</span><span class="token method function property-access">replaceWithText</span><span class="token punctuation">(</span><span class="token template-string template-punctuation string">`</span><span class="token template-string string">{ where: </span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">paramAsTxt</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string string"> }</span><span class="token template-string template-punctuation string">`</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">That's all the pieces we need! The approach will mess up indentation in source code, but fortunately there's <code>eslint --fix</code>! After the refactor operation, all formatting trouble should be possible to fix with a single eslint command.</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="the-result">The result</h2><p class="max-w-xl font-sans mb-paragraph ">Everything in a ready-to-go TS script.</p><pre class="py-0"><code class="language-ts"><div class="token-line"><span class="token keyword">import</span><span class="token plain"> </span><span class="token imports">chalk</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">'chalk'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token imports">path</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">'path'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">import</span><span class="token plain"> </span><span class="token imports punctuation">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">AwaitExpression</span><span class="token imports punctuation">,</span><span class="token imports"> </span><span class="token imports maybe-class-name">Project</span><span class="token imports punctuation">,</span><span class="token imports"> </span><span class="token imports maybe-class-name">SyntaxKind</span><span class="token imports"> </span><span class="token imports punctuation">}</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">'ts-morph'</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">void</span><span class="token plain"> </span><span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">async</span><span class="token plain"> </span><span class="token keyword">function</span><span class="token plain"> </span><span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">'Refactoring ..\n'</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">const</span><span class="token plain"> project </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword">new</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Project</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  project</span><span class="token punctuation">.</span><span class="token method function property-access">addSourceFilesAtPaths</span><span class="token punctuation">(</span><span class="token string">'src/**/*.ts'</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">const</span><span class="token plain"> sourceFiles </span><span class="token operator">=</span><span class="token plain"> project</span><span class="token punctuation">.</span><span class="token method function property-access">getSourceFiles</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token comment">// To debug typescript code AST: https://ts-ast-viewer.com/</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">for</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token keyword">const</span><span class="token plain"> file </span><span class="token keyword">of</span><span class="token plain"> sourceFiles</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token keyword">const</span><span class="token plain"> filePath </span><span class="token operator">=</span><span class="token plain"> path</span><span class="token punctuation">.</span><span class="token method function property-access">relative</span><span class="token punctuation">(</span><span class="token plain">process</span><span class="token punctuation">.</span><span class="token method function property-access">cwd</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token plain"> file</span><span class="token punctuation">.</span><span class="token method function property-access">getFilePath</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token plain">chalk</span><span class="token punctuation">.</span><span class="token method function property-access">bold</span><span class="token punctuation">(</span><span class="token plain">filePath</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">    </span><span class="token keyword">const</span><span class="token plain"> awaits </span><span class="token operator">=</span><span class="token plain"> file</span><span class="token punctuation">.</span><span class="token method function property-access">getDescendantsOfKind</span><span class="token punctuation">(</span><span class="token maybe-class-name">SyntaxKind</span><span class="token punctuation">.</span><span class="token property-access maybe-class-name">AwaitExpression</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token keyword">for</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token keyword">const</span><span class="token plain"> awaitExpression </span><span class="token keyword">of</span><span class="token plain"> awaits</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token keyword">if</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">shouldRefactor</span><span class="token punctuation">(</span><span class="token plain">awaitExpression</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">        </span><span class="token keyword">continue</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">      </span><span class="token function">refactor</span><span class="token punctuation">(</span><span class="token plain">awaitExpression</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">    </span><span class="token keyword">await</span><span class="token plain"> file</span><span class="token punctuation">.</span><span class="token method function property-access">save</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">function</span><span class="token plain"> </span><span class="token function">shouldRefactor</span><span class="token punctuation">(</span><span class="token plain">awaitExpression</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">AwaitExpression</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">const</span><span class="token plain"> paExpression </span><span class="token operator">=</span><span class="token plain"> awaitExpression</span><span class="token punctuation">.</span><span class="token method function property-access">getFirstDescendantByKind</span><span class="token punctuation">(</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token maybe-class-name">SyntaxKind</span><span class="token punctuation">.</span><span class="token property-access maybe-class-name">PropertyAccessExpression</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">if</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token operator">!</span><span class="token plain">paExpression </span><span class="token operator">||</span><span class="token plain"> paExpression</span><span class="token punctuation">.</span><span class="token method function property-access">wasForgotten</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">// In certain cases ts-morph stops tracking nodes for performance</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token comment">//  optimisation reasons. If the node was forgotten, accessing it throws.</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token keyword">return</span><span class="token plain"> </span><span class="token boolean">false</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">const</span><span class="token plain"> name </span><span class="token operator">=</span><span class="token plain"> paExpression</span><span class="token punctuation">.</span><span class="token method function property-access">getName</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">const</span><span class="token plain"> entityMethods </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation">[</span><span class="token string">'find'</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">'maybeFind'</span><span class="token punctuation">,</span><span class="token plain"> </span><span class="token string">'findMany'</span><span class="token punctuation">]</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">return</span><span class="token plain"> entityMethods</span><span class="token punctuation">.</span><span class="token method function property-access">includes</span><span class="token punctuation">(</span><span class="token plain">name</span><span class="token operator">!</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain"></span><span class="token keyword">function</span><span class="token plain"> </span><span class="token function">refactor</span><span class="token punctuation">(</span><span class="token plain">awaitExpression</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">AwaitExpression</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">const</span><span class="token plain"> param </span><span class="token operator">=</span><span class="token plain"> awaitExpression</span><span class="token punctuation">.</span><span class="token method function property-access">getFirstDescendantByKind</span><span class="token punctuation">(</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token maybe-class-name">SyntaxKind</span><span class="token punctuation">.</span><span class="token property-access maybe-class-name">ObjectLiteralExpression</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">if</span><span class="token plain"> </span><span class="token punctuation">(</span><span class="token operator">!</span><span class="token plain">param</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token punctuation">{</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">    </span><span class="token keyword">throw</span><span class="token plain"> </span><span class="token keyword">new</span><span class="token plain"> </span><span class="token class-name known-class-name class-name">Error</span><span class="token punctuation">(</span><span class="token template-string template-punctuation string">`</span><span class="token template-string string">SyntaxKind.ObjectLiteralExpression not found</span><span class="token template-string template-punctuation string">`</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  </span><span class="token punctuation">}</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">
</span></div><div class="token-line"><span class="token plain">  </span><span class="token keyword">const</span><span class="token plain"> paramAsTxt </span><span class="token operator">=</span><span class="token plain"> param</span><span class="token punctuation">.</span><span class="token method function property-access">getText</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token plain"> </span><span class="token comment">// e.g. "{ name: 'Lisa' }"</span><span class="token plain"></span></div><div class="token-line"><span class="token plain">  param</span><span class="token punctuation">.</span><span class="token method function property-access">replaceWithText</span><span class="token punctuation">(</span><span class="token template-string template-punctuation string">`</span><span class="token template-string string">{ where: </span><span class="token template-string interpolation interpolation-punctuation punctuation">${</span><span class="token template-string interpolation">paramAsTxt</span><span class="token template-string interpolation interpolation-punctuation punctuation">}</span><span class="token template-string string"> }</span><span class="token template-string template-punctuation string">`</span><span class="token punctuation">)</span><span class="token plain"></span></div><div class="token-line"><span class="token plain"></span><span class="token punctuation">}</span><span class="token plain"></span></div></code></pre><p class="max-w-xl font-sans mb-paragraph ">Mass refactoring TypeScript is surprisingly convenient using ts-morph. You wouldn't want to do this for every small change, but it's worth considering for larger refactoring piles.</p><p class="max-w-xl font-sans mb-paragraph ">This time the automated approach took me more than the manual would've. But next time...</p><h2 class="text-3xl mt-12 mb-5 font-heading font-black text-gray-9 dark:text-gray-3" id="prior-art">Prior art</h2><ul class="pl-6 list-disc space-y-1"><li class="md:pl-2">Write an auto-fixable ESLint rule and apply</li><li class="md:pl-2"><a href="https://github.com/facebook/jscodeshift" class="cursor-pointer underline hover:underline ">jscodeshift</a></li></ul></div></body></html>]]></content:encoded><content:media url="https://kimmo.blog/content-assets/8/cover.jpg" type="image/jpeg" medium="image" xmlns:media="http://search.yahoo.com/mrss/"/></item></channel></rss>