<?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[Shankproof]]></title><description><![CDATA[Shankproof]]></description><link>https://shankproof.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 10 Apr 2026 17:47:57 GMT</lastBuildDate><atom:link href="https://shankproof.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[What’s in the Bag: Java, But Make It Modern]]></title><description><![CDATA[Part 1 of a new series on JVM technologies, Kotlin, and the tools worth your time in 2025.
For the past three decades, I’ve had a complicated relationship with Java. It was one of the first languages I wrote “real” software in, but over the years I d...]]></description><link>https://shankproof.dev/whats-in-the-bag-java-but-make-it-modern</link><guid isPermaLink="true">https://shankproof.dev/whats-in-the-bag-java-but-make-it-modern</guid><category><![CDATA[Java]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[kotlin beginner]]></category><category><![CDATA[jvm]]></category><category><![CDATA[programming languages]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Types]]></category><category><![CDATA[Jetbrains]]></category><category><![CDATA[Modern Java]]></category><category><![CDATA[ktor]]></category><category><![CDATA[Build In Public]]></category><dc:creator><![CDATA[Ryan Bell]]></dc:creator><pubDate>Fri, 23 May 2025 02:01:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747965288902/6917e6c6-ed96-4d8f-bd19-e5f90667d144.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Part 1 of a new series on JVM technologies, Kotlin, and the tools worth your time in 2025.</em></p>
<p>For the past three decades, I’ve had a complicated relationship with Java. It was one of the first languages I wrote “real” software in, but over the years I drifted toward more expressive, flexible ecosystems — Ruby, Elixir, TypeScript. Java always felt like something you had to fight a little to enjoy.</p>
<p>But lately, something’s shifted.</p>
<p>I’ve been circling back to the JVM, not because I miss Java, but because I’ve been pulled in by <strong>Kotlin</strong> — a language that manages to feel modern, expressive, and dare I say… joyful?</p>
<p>If Java is the multi-tool that gets the job done, Kotlin is the well-balanced wedge that just feels right in your hand.</p>
<hr />
<h2 id="heading-so-whats-this-series">So what’s this series?</h2>
<p>This “What’s in the Bag” JVM series is my way of exploring the tools and ideas that are making Java exciting again — at least for me. I'm focusing less on traditional enterprise stacks and more on <strong>lightweight, elegant technologies</strong> that feel closer to the kind of development I enjoy in TypeScript.</p>
<p>Here’s what you can expect:</p>
<ol>
<li><p><strong>A Kotlin primer</strong> – Why I think it’s the best thing I’ve used on the JVM in a long time.</p>
</li>
<li><p><strong>A look at Ktor</strong> – A JetBrains-built server framework that makes writing APIs in Kotlin surprisingly fun.</p>
</li>
<li><p><strong>Some working examples</strong> – APIs, dev tools, and patterns that remind me of the joy of fast iteration.</p>
</li>
<li><p><strong>A few comparisons</strong> – Why these tools are worth a second look even if you’ve moved past Java.</p>
</li>
</ol>
<hr />
<h2 id="heading-why-kotlin">Why Kotlin?</h2>
<p>Kotlin hits a sweet spot that few languages do:</p>
<ul>
<li><p><strong>Null safety that works</strong></p>
</li>
<li><p><strong>Coroutines and structured concurrency</strong></p>
</li>
<li><p><strong>First-class tooling from JetBrains</strong></p>
</li>
<li><p><strong>Concise syntax with real type safety</strong></p>
</li>
</ul>
<p>And maybe most surprisingly — it’s just <em>fun</em> to write.</p>
<p>If you’ve ever thought <em>“I wish JavaScript had a type system that didn’t hate me”</em>, or <em>“I wish TypeScript ran on the server with real threads”</em>, Kotlin might be your thing.</p>
<hr />
<h2 id="heading-where-this-is-going">Where this is going</h2>
<p>I’m going to keep each entry focused, digestible, and grounded in real code. The goal isn’t to teach Kotlin from scratch — it’s to show what makes these tools <strong>worth your time</strong> in 2025, even if you never thought you’d look at a <code>.kt</code> file again.</p>
<p>Let’s get back in the bag.</p>
<hr />
<h3 id="heading-next-up-kotlin-for-typescript-developers">Next Up: Kotlin for TypeScript Developers</h3>
<p>We'll explore Kotlin's core features, what makes it elegant, and how it compares to the tools you already know.</p>
]]></content:encoded></item><item><title><![CDATA[What Really Lowers Your Score? Modeling the Truth Behind Strokes Gained]]></title><description><![CDATA[🏌️ What Really Lowers Your Score?
I’ve always been curious about what really lowers golf scores.
Not just what feels important — but what the data actually says.
So I started building a model.And I’m already learning things that surprised me.

📊 No...]]></description><link>https://shankproof.dev/what-really-lowers-your-score-modeling-the-truth-behind-strokes-gained</link><guid isPermaLink="true">https://shankproof.dev/what-really-lowers-your-score-modeling-the-truth-behind-strokes-gained</guid><category><![CDATA[golf data]]></category><category><![CDATA[strokes gained analysis]]></category><category><![CDATA[pga data science]]></category><category><![CDATA[strokes gained]]></category><category><![CDATA[lower golf scores]]></category><category><![CDATA[golf analytics]]></category><category><![CDATA[golf performance model]]></category><category><![CDATA[golf stats interpretation]]></category><dc:creator><![CDATA[Ryan Bell]]></dc:creator><pubDate>Wed, 23 Apr 2025 03:04:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745378147313/c51d22d1-fb0a-46c4-b526-7e35872b2004.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-what-really-lowers-your-score">🏌️ What Really Lowers Your Score?</h2>
<p>I’ve always been curious about what <em>really</em> lowers golf scores.</p>
<p>Not just what feels important — but what the data actually says.</p>
<p>So I started building a model.<br />And I’m already learning things that surprised me.</p>
<hr />
<h2 id="heading-not-all-strokes-are-equal">📊 Not All Strokes Are Equal</h2>
<p>Strokes Gained is a powerful stat.<br />But at its core, it assumes one thing:</p>
<blockquote>
<p>A stroke gained is a stroke gained, no matter where you earn it.</p>
</blockquote>
<p>That might be true in theory.<br />But what if <em>how</em> you gain or lose strokes matters more than we think?</p>
<hr />
<h2 id="heading-youve-heard-the-debate">🧠 You've Heard the Debate:</h2>
<blockquote>
<p><em>“Drive for show, putt for dough.”</em><br /><em>“The driver is the most important club in the bag.”</em></p>
</blockquote>
<p>Depending on who you ask, the most valuable club in your bag changes.</p>
<p>I wanted to know what the <strong>data</strong> says — not just the tour chatter.</p>
<p>So I built a model and ran it on <strong>7 full seasons of PGA Tour data, from 2015 to 2022</strong>, using round-level strokes gained stats and computed scoring differentials.</p>
<hr />
<h2 id="heading-early-results-from-the-pga-tour">📈 Early Results from the PGA Tour</h2>
<p>Here’s what I found:</p>
<ul>
<li><p>✅ <strong>Approach play</strong> is the most consistent driver of lower scores</p>
</li>
<li><p>✅ <strong>Putting</strong> is right behind — and sometimes just as important</p>
</li>
<li><p>✅ <strong>Off-the-tee skill</strong> helps, but contributes less directly</p>
</li>
<li><p>✅ <strong>Short game</strong> plays a supporting role, not a starring one</p>
</li>
</ul>
<p>That’s the big picture.<br />But when you zoom in on individual players, the story gets more interesting.</p>
<hr />
<h2 id="heading-everyone-scores-differently">🔍 Everyone Scores Differently</h2>
<p>I ran individual models on dozens of PGA players — and the variation was striking.</p>
<ul>
<li><p>🧨 Some players like <strong>Erik van Rooyen</strong> and <strong>Daniel Chopra</strong> rely heavily on Off-the-Tee or Short Game performance to lower scores</p>
</li>
<li><p>🎯 Others like <strong>Victor Perez</strong> or <strong>Mark Wilson</strong> live and die by the putter</p>
</li>
<li><p>🛠 Players like <strong>Rory McIlroy</strong> and <strong>Sean O’Hair</strong> show balanced, all-around scoring profiles</p>
</li>
</ul>
<blockquote>
<p>Every scorer has a different recipe.<br />And the model shows how they get it done.</p>
</blockquote>
<hr />
<h2 id="heading-how-you-can-help">🗣 How You Can Help</h2>
<p>Now I want to go further.</p>
<p>I'm starting to collect <strong>amateur and VR golf data</strong> to see how these patterns shift at lower skill levels.</p>
<ul>
<li><p>Are drivers more important when you’re missing more greens?</p>
</li>
<li><p>Does putting get messier as you move away from elite ranks?</p>
</li>
<li><p>Do different skill levels <em>need different practice priorities</em>?</p>
</li>
</ul>
<p>If you're a golfer — real-world or VR — and want to help, I’d love your input.</p>
<p>Even just a few rounds of data — Score, Differential, and basic strokes gained stats — can help expand the model.</p>
<blockquote>
<p><strong>Want to contribute?</strong><br />A simple upload form is coming soon. Until then, feel free to reach out.</p>
</blockquote>
<hr />
<h2 id="heading-whats-coming-next">🧠 What’s Coming Next</h2>
<p>In future posts, I’ll be sharing:</p>
<ul>
<li><p>How PGA Tour players actually score — by skill, not by myth</p>
</li>
<li><p>What patterns emerge at the amateur level</p>
</li>
<li><p>What this means for your practice, your stats, and your game</p>
</li>
</ul>
<p>This is just the beginning.<br />Let’s find out what really matters — and maybe help a few golfers score lower along the way.</p>
<hr />
<p>📝 <em>Want to follow along or contribute your own rounds? You can find me at</em> <a target="_blank" href="https://shankproof.dev"><em>shankproof.dev</em></a> <em>or drop a comment below.</em><br />👊</p>
]]></content:encoded></item><item><title><![CDATA[What’s in the Bag? Building Fairway Tasks with Deno, Fresh, and SSE]]></title><description><![CDATA[In Part 1 of this series, I introduced the idea of looking at development stacks the way golfers look at their gear. Every tool in the bag has a purpose. Some we reach for without thinking. Others we try out, experiment with, and either adopt or toss...]]></description><link>https://shankproof.dev/whats-in-the-bag-building-fairway-tasks-with-deno-fresh-and-sse</link><guid isPermaLink="true">https://shankproof.dev/whats-in-the-bag-building-fairway-tasks-with-deno-fresh-and-sse</guid><category><![CDATA[minimal-stack]]></category><category><![CDATA[Deno]]></category><category><![CDATA[fresh]]></category><category><![CDATA[SSE]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Build In Public]]></category><category><![CDATA[realtime]]></category><category><![CDATA[full stack]]></category><dc:creator><![CDATA[Ryan Bell]]></dc:creator><pubDate>Sun, 13 Apr 2025 03:19:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744514131480/bb15a963-d0a7-4979-85de-cc6815864bb1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In <a target="_blank" href="https://shankproof.dev/whats-in-the-bag?source=more_series_bottom_blogs">Part 1 of this series</a>, I introduced the idea of looking at development stacks the way golfers look at their gear. Every tool in the bag has a purpose. Some we reach for without thinking. Others we try out, experiment with, and either adopt or toss aside. This week, I finally teed off on the first build: <strong>Fairway Tasks</strong>, a collaborative to-do app powered by <strong>Deno</strong>, <strong>Fresh</strong>, and <strong>Server-Sent Events (SSE)</strong>.</p>
<p>The goal was to build a real-world demo that shows off the strengths of this stack—without turning it into a full-blown product. I wanted persistence, live updates, and minimal complexity.</p>
<hr />
<h3 id="heading-what-were-building">What We’re Building</h3>
<p>Fairway Tasks is a collaborative to-do list that allows anyone to:</p>
<ul>
<li><p>Add tasks</p>
</li>
<li><p>Mark them as complete</p>
</li>
<li><p>See updates in real-time across all open tabs</p>
</li>
</ul>
<p>There’s no login system, no multi-user tracking, and no external database. Just the core interactions and live state syncing.</p>
<p>But there <em>is</em> one key addition to the base demo: I used <strong>Deno’s built-in KV store</strong> to persist the data. It’s a small change from an in-memory store, but it showcases how batteries-included Deno really is.</p>
<hr />
<h3 id="heading-the-stack">The Stack</h3>
<p>Here’s what I used:</p>
<ul>
<li><p><strong>Deno</strong>: runtime with built-in TypeScript, security, testing, and KV storage</p>
</li>
<li><p><strong>Fresh</strong>: a modern web framework with island-based rendering</p>
</li>
<li><p><strong>SSE (Server-Sent Events)</strong>: for real-time updates</p>
</li>
<li><p><strong>KV Store</strong>: Deno’s built-in persistence layer</p>
</li>
</ul>
<p>Together, this gave me a stack with no extra tooling or dependency setup. Just native capabilities and good architecture choices.</p>
<hr />
<h3 id="heading-diving-into-the-code">Diving into the Code</h3>
<p>Fairway Tasks has a minimal but purposeful codebase. Here’s a closer look at the moving parts and how they interact.</p>
<p><strong>1. API Routes</strong></p>
<p>All task interactions live in <code>routes/api/tasks.ts</code>. It handles:</p>
<ul>
<li><p><code>GET</code> – Return all tasks from the Deno KV store</p>
</li>
<li><p><code>POST</code> – Create a new task, store it, and broadcast it</p>
</li>
<li><p><code>PATCH</code> – Mark a task complete and broadcast the update</p>
</li>
<li><p><code>DELETE</code> – Remove a task from storage and notify all clients</p>
</li>
</ul>
<p>All task mutations trigger the <code>broadcast()</code> function from <code>routes/api/stream.ts</code>, sending the action over Server-Sent Events to connected tabs.</p>
<p><strong>2. Server-Sent Events (SSE)</strong></p>
<p>The real-time update system is handled via a simple SSE implementation:</p>
<ul>
<li><p>Clients connect to <code>/api/stream</code></p>
</li>
<li><p>A <code>Set</code> of writable clients is maintained</p>
</li>
<li><p>On any task change, a message is encoded and sent to each connected writer</p>
</li>
</ul>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">broadcast</span>(<span class="hljs-params">data: unknown</span>) </span>{
  <span class="hljs-keyword">const</span> msg = <span class="hljs-string">`data: <span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify(data)}</span>\n\n`</span>;
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> writer <span class="hljs-keyword">of</span> clients) {
    writer.write(<span class="hljs-keyword">new</span> TextEncoder().encode(msg));
  }
}
</code></pre>
<p>It’s lightweight, doesn’t require any external library, and perfect for small collaborative experiences.</p>
<p><strong>3. Frontend Islands</strong></p>
<p>The interactive frontend lives in <code>islands/TaskInput.tsx</code>, where:</p>
<ul>
<li><p>Tasks are displayed from initial props</p>
</li>
<li><p>Users can add, complete, or delete tasks with a minimal UI</p>
</li>
<li><p>The island listens to the SSE stream and updates local state accordingly</p>
</li>
</ul>
<p>With the recent addition of <a target="_blank" href="https://github.com/kofno/jsonous">Jsonous</a>, the SSE message decoding now looks like this:</p>
<pre><code class="lang-typescript">useEffect(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> sse = <span class="hljs-keyword">new</span> EventSource(<span class="hljs-string">"/api/stream"</span>);
  sse.onmessage = <span class="hljs-function">(<span class="hljs-params">msg</span>) =&gt;</span> {
    sseEventDecoder.decodeJson(msg.data).cata({
      Ok: <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
        <span class="hljs-keyword">if</span> (event.type === <span class="hljs-string">"add"</span>) {
          setTasks(<span class="hljs-function">(<span class="hljs-params">prev</span>) =&gt;</span> [...prev, event.task]);
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (event.type === <span class="hljs-string">"complete"</span>) {
          setTasks(<span class="hljs-function">(<span class="hljs-params">prev</span>) =&gt;</span>
            prev.map(<span class="hljs-function">(<span class="hljs-params">t</span>) =&gt;</span> t.id === event.id ? { ...t, completed: <span class="hljs-literal">true</span> } : t)
          );
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (event.type === <span class="hljs-string">"delete"</span>) {
          setTasks(<span class="hljs-function">(<span class="hljs-params">prev</span>) =&gt;</span> prev.filter(<span class="hljs-function">(<span class="hljs-params">t</span>) =&gt;</span> t.id !== event.id));
        }
      },
      Err: <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
        <span class="hljs-built_in">console</span>.error(err);
      },
    });
  };
  <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> sse.close();
}, []);
</code></pre>
<p>This small change brings clarity, safety, and makes the SSE message format easier to evolve over time.</p>
<p><strong>Bonus: Deno KV Integration</strong></p>
<p>Persistence is handled with Deno’s built-in KV store using a <code>utils/kv.ts</code> helper module. Tasks are saved under a <code>task:&lt;id&gt;</code> key. Fetching is done with a simple <code>kv.list()</code> call.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTasks</span>(<span class="hljs-params"></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">Task</span>[]&gt; </span>{
  <span class="hljs-keyword">const</span> entries = [];
  <span class="hljs-keyword">for</span> <span class="hljs-keyword">await</span> (<span class="hljs-keyword">const</span> res <span class="hljs-keyword">of</span> kv.list&lt;Task&gt;({ prefix: [<span class="hljs-string">"task"</span>] })) {
    entries.push(res.value);
  }
  <span class="hljs-keyword">return</span> entries;
}
</code></pre>
<p>The result? A real-time, collaborative app with no third-party DB or state store.</p>
<h3 id="heading-what-worked-well">What Worked Well</h3>
<ul>
<li><p><strong>Fresh’s simplicity</strong>: Routing, rendering, and hydration are all predictable and fast</p>
</li>
<li><p><strong>SSE was perfect</strong>: Minimal setup, great fit for one-way sync like this</p>
</li>
<li><p><strong>Deno’s DX</strong>: Linting, testing, and dev server all just work</p>
</li>
<li><p><strong>KV store</strong>: Simple, stable, and lets the app persist between restarts with almost no config</p>
</li>
</ul>
<hr />
<h3 id="heading-what-id-explore-next">What I’d Explore Next</h3>
<ul>
<li><p><strong>Adding authentication</strong> using session cookies or tokens</p>
</li>
<li><p><strong>Using external DBs</strong> if relational needs increase</p>
</li>
<li><p><strong>More complex event handling</strong> (editing tasks, reordering, undo)</p>
</li>
<li><p><strong>Offline sync and background queuing</strong></p>
</li>
<li><p><strong>More Robust SSE</strong> (something that scales horizontally and watches the db for changes)</p>
</li>
</ul>
<hr />
<h3 id="heading-wrap-up">Wrap-up</h3>
<p>Fairway Tasks started as a small project—a swing at trying something simple, collaborative, and live. It turned into a rewarding dive into the capabilities of Deno and Fresh. Using only built-in tools like the KV store and SSE, I was able to build a functional app with real-time syncing and data persistence, all without touching external services or bolting on extra complexity.</p>
<p>The island-based approach of Fresh made it easy to separate server-rendered state from interactive components, and adding <code>jsonous</code> brought confidence to decoding live updates. It’s refreshing to build something that feels cohesive, productive, and fun.</p>
<p>There’s still room to grow—authentication, offline support, and more resilient streaming—but as it stands, Fairway Tasks is a solid example of what modern minimal stacks can accomplish.</p>
<p>You can try the app <a target="_blank" href="https://ryanlbell-fairway-tas-96.deno.dev/">live on Deno Deploy</a>, and view the source code on <a target="_blank" href="https://github.com/kofno/fairway-tasks">GitHub</a>.</p>
<p>Next up? I might take a swing with <strong>Wasp</strong>, <strong>Effect</strong>, or even try a mobile-first build using <strong>Tamagui</strong>. Let me know what you'd like to see in the next post—and what’s in <em>your</em> bag these days.</p>
<p>Thanks for reading!</p>
]]></content:encoded></item><item><title><![CDATA[Streamlined Discriminated Union Decoding in TypeScript with jsonous's New Decoder]]></title><description><![CDATA[TypeScript developers love discriminated unions (or tagged unions). They provide a fantastic way to model states, events, or different kinds of data structures in a type-safe manner. When working with external data sources like JSON APIs, however, de...]]></description><link>https://shankproof.dev/streamlined-discriminated-union-decoding-in-typescript-with-jsonouss-new-decoder</link><guid isPermaLink="true">https://shankproof.dev/streamlined-discriminated-union-decoding-in-typescript-with-jsonouss-new-decoder</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[json]]></category><category><![CDATA[Discriminated Union Types]]></category><category><![CDATA[Validation]]></category><category><![CDATA[TypeSafety]]></category><category><![CDATA[Functional Programming]]></category><category><![CDATA[error handling]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Ryan Bell]]></dc:creator><pubDate>Sat, 12 Apr 2025 03:57:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744430037053/1ead4c9c-03ae-4554-beff-3447913421fc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>TypeScript developers love discriminated unions (or tagged unions). They provide a fantastic way to model states, events, or different kinds of data structures in a type-safe manner. When working with external data sources like JSON APIs, however, decoding these unions reliably can sometimes feel a bit cumbersome.</p>
<p>Here at <code>jsonous</code>, we aim to make JSON decoding as painless and type-safe as possible. While our existing <code>oneOf</code> decoder could handle unions, it wasn't specifically optimized for the common discriminated union pattern. Today, we're excited to introduce a new tool designed precisely for this job: the <code>discriminatedUnion</code> decoder!</p>
<h3 id="heading-the-challenge-decoding-discriminated-unions-the-old-way">The Challenge: Decoding Discriminated Unions "The Old Way"</h3>
<p>Let's consider a common example: representing different types of users in our system.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Our TypeScript types</span>
<span class="hljs-keyword">interface</span> User {
  <span class="hljs-keyword">type</span>: <span class="hljs-string">'user'</span>;
  id: <span class="hljs-built_in">string</span>;
  name: <span class="hljs-built_in">string</span>;
  isActive: <span class="hljs-built_in">boolean</span>;
}

<span class="hljs-keyword">interface</span> Admin {
  <span class="hljs-keyword">type</span>: <span class="hljs-string">'admin'</span>;
  id: <span class="hljs-built_in">string</span>;
  name: <span class="hljs-built_in">string</span>;
  permissions: <span class="hljs-built_in">string</span>[];
}

<span class="hljs-keyword">type</span> Person = User | Admin;
</code></pre>
<p>We have a <code>Person</code> type which can be either a <code>User</code> or an <code>Admin</code>, distinguished by the <code>type</code> field.</p>
<p>Using <code>jsonous</code> previously, you'd typically define decoders for each variant and combine them with <code>oneOf</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {
  <span class="hljs-built_in">string</span>,
  <span class="hljs-built_in">boolean</span>,
  array,
  stringLiteral,
  createDecoderFromStructure,
  oneOf,
  Decoder,
  identity <span class="hljs-comment">// Needed for mapping</span>
} <span class="hljs-keyword">from</span> <span class="hljs-string">'jsonous'</span>;

<span class="hljs-comment">// Decoders for each variant</span>
<span class="hljs-keyword">const</span> userDecoder: Decoder&lt;User&gt; = createDecoderFromStructure({
  <span class="hljs-keyword">type</span>: stringLiteral(<span class="hljs-string">'user'</span>),
  id: <span class="hljs-built_in">string</span>,
  name: <span class="hljs-built_in">string</span>,
  isActive: <span class="hljs-built_in">boolean</span>,
});

<span class="hljs-keyword">const</span> adminDecoder: Decoder&lt;Admin&gt; = createDecoderFromStructure({
  <span class="hljs-keyword">type</span>: stringLiteral(<span class="hljs-string">'admin'</span>),
  id: <span class="hljs-built_in">string</span>,
  name: <span class="hljs-built_in">string</span>,
  permissions: array(<span class="hljs-built_in">string</span>),
});

<span class="hljs-comment">// --- The "Old Way" using oneOf ---</span>
<span class="hljs-keyword">const</span> personDecoderOneOf: Decoder&lt;Person&gt; = oneOf([
  <span class="hljs-comment">// We need to explicitly map each decoder to the union type</span>
  userDecoder.map&lt;Person&gt;(identity),
  adminDecoder.map&lt;Person&gt;(identity),
]);
</code></pre>
<p>This works, but has a few drawbacks:</p>
<ol>
<li><p><strong>Verbosity:</strong> You need <code>.map&lt;Person&gt;(identity)</code> for every single variant. This adds boilerplate, especially with many variants.</p>
</li>
<li><p><strong>Less Specific Errors:</strong> If decoding fails, <code>oneOf</code> tries <em>every</em> decoder in the list and reports <em>all</em> failures. For a discriminated union, you often intuitively know <em>which</em> variant <em>should</em> have matched based on the <code>type</code> field, making the other errors noise.</p>
</li>
<li><p><strong>Potential Inefficiency:</strong> <code>oneOf</code> might run multiple potentially complex decoders even if the <code>type</code> field clearly indicates only one is relevant.</p>
</li>
</ol>
<h3 id="heading-introducing-discriminatedunion-the-right-tool-for-the-job">Introducing <code>discriminatedUnion</code>: The Right Tool for the Job</h3>
<p>The new <code>discriminatedUnion</code> decoder is designed to address these pain points directly. It leverages the discriminator field (<code>type</code> in our case) to intelligently select and run the correct decoder.</p>
<p><strong>Here's how it works:</strong></p>
<ol>
<li><p>You tell it the name of the <code>discriminatorField</code> (e.g., <code>"type"</code>).</p>
</li>
<li><p>You provide a <code>mapping</code> object where keys are the possible string values of the discriminator (e.g., <code>"user"</code>, <code>"admin"</code>) and values are the corresponding decoders for each variant.</p>
</li>
<li><p>It first decodes <em>only</em> the discriminator field.</p>
</li>
<li><p>Based on the value found, it looks up the correct decoder in your mapping.</p>
</li>
<li><p>It runs <em>only that specific decoder</em> on the original input.</p>
</li>
</ol>
<p>Let's rewrite our <code>Person</code> decoder using <code>discriminatedUnion</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {
  <span class="hljs-comment">// ... other imports remain the same ...</span>
  discriminatedUnion <span class="hljs-comment">// Import the new decoder</span>
} <span class="hljs-keyword">from</span> <span class="hljs-string">'jsonous'</span>;

<span class="hljs-comment">// userDecoder and adminDecoder definitions remain the same...</span>

<span class="hljs-comment">// --- The "New Way" using discriminatedUnion ---</span>
<span class="hljs-keyword">const</span> personDecoder: Decoder&lt;Person&gt; = discriminatedUnion(<span class="hljs-string">'type'</span>, {
  user: userDecoder, <span class="hljs-comment">// Key matches the 'type' value</span>
  admin: adminDecoder, <span class="hljs-comment">// Key matches the 'type' value</span>
});

<span class="hljs-comment">// Type check: personDecoder is automatically Decoder&lt;Person&gt; - no mapping needed!</span>
</code></pre>
<p>Look how much cleaner that is! No more <code>.map(identity)</code>. The decoder's type <code>Decoder&lt;Person&gt;</code> is inferred automatically from the provided mapping.</p>
<h3 id="heading-why-discriminatedunion-is-better">Why <code>discriminatedUnion</code> is Better</h3>
<p>This new decoder offers significant advantages:</p>
<ol>
<li><p><strong>Type Safety:</strong> Automatically infers the correct union type (<code>User | Admin</code> in this case) without manual type hints in <code>.map</code>.</p>
</li>
<li><p><strong>Conciseness:</strong> Eliminates the repetitive <code>.map(identity)</code> calls, making your decoder definitions cleaner and easier to read.</p>
</li>
<li><p><strong>Clarity:</strong> The structure <code>discriminatedUnion('field', { key1: decoder1, key2: decoder2 })</code> clearly expresses the intent of choosing a decoder based on a specific field's value.</p>
</li>
<li><p><strong>Targeted Errors:</strong> Error messages are much more helpful. Instead of trying all decoders, it fails fast with specific reasons:</p>
<ul>
<li><p>Did the discriminator field (<code>type</code>) exist and was it a string?</p>
</li>
<li><p>Was the value of the discriminator field (<code>"user"</code>, <code>"admin"</code>) one of the expected keys in the mapping?</p>
</li>
<li><p>Did the <em>selected</em> variant decoder (<code>userDecoder</code> or <code>adminDecoder</code>) fail?</p>
</li>
</ul>
</li>
<li><p><strong>Efficiency:</strong> It avoids running unnecessary decoders. It only decodes the simple discriminator field first and then runs exactly one variant decoder.</p>
</li>
</ol>
<h3 id="heading-a-clearer-picture-error-handling">A Clearer Picture: Error Handling</h3>
<p>Let's see the difference in error messages. Consider this invalid input:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> invalidData = { <span class="hljs-keyword">type</span>: <span class="hljs-string">'guest'</span>, id: <span class="hljs-string">'guest-001'</span> };
</code></pre>
<p><strong>Error with</strong> <code>oneOf</code>:</p>
<pre><code class="lang-plaintext">// personDecoderOneOf.decodeAny(invalidData) might produce:
Err: I found the following problems:
Expected user but got "guest":
occurred in a field named 'type'
Expected admin but got "guest":
occurred in a field named 'type'
</code></pre>
<p>(It tells you <em>both</em> decoders failed because the <code>type</code> was wrong).</p>
<p><strong>Error with</strong> <code>discriminatedUnion</code>:</p>
<pre><code class="lang-plaintext">// personDecoder.decodeAny(invalidData) produces:
Err: Unexpected discriminator value 'guest' for field 'type'. Expected one of: user, admin. Found in: {"type":"guest","id":"guest-001"}
</code></pre>
<p>(It tells you <em>exactly</em> the problem: the value <code>"guest"</code> wasn't expected for the <code>type</code> field).</p>
<p>If the <code>type</code> was correct but other data was wrong (e.g., <code>isActive</code> was a string for a <code>user</code>), <code>discriminatedUnion</code> would report the error <em>from within the</em> <code>userDecoder</code>, prefixed clearly:</p>
<pre><code class="lang-plaintext">// Example: { type: 'user', id: 'u1', name: 'Test', isActive: 'yes' }
Err: Error decoding variant with type='user': I expected to find a boolean but instead I found "yes":
occurred in a field named 'isActive'
</code></pre>
<h3 id="heading-get-started-today">Get Started Today!</h3>
<p>Decoding discriminated unions is now simpler, safer, and more efficient in <code>jsonous</code>. If you're working with tagged unions and JSON, the <code>discriminatedUnion</code> decoder is the tool you've been waiting for.</p>
<p>Update <code>jsonous</code> to the latest version and give it a try! We think you'll appreciate the improved ergonomics and clearer error reporting. Check out the README for detailed usage and examples.</p>
<p>Happy Decoding!</p>
]]></content:encoded></item><item><title><![CDATA[What's in the Bag?]]></title><description><![CDATA[Golfers know the phrase "what's in the bag?"—it’s shorthand for talking about the tools they trust when they head out on the course. From drivers to wedges to that one beat-up hybrid they swear by, every club has a role, and every bag tells a story.
...]]></description><link>https://shankproof.dev/whats-in-the-bag</link><guid isPermaLink="true">https://shankproof.dev/whats-in-the-bag</guid><category><![CDATA[TechStack]]></category><category><![CDATA[Deno]]></category><category><![CDATA[fresh]]></category><category><![CDATA[learning]]></category><category><![CDATA[Learning Journey]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Ryan Bell]]></dc:creator><pubDate>Wed, 09 Apr 2025 02:36:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744165471068/13bc29aa-1059-4fe6-8bd9-f43391444485.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Golfers know the phrase "what's in the bag?"—it’s shorthand for talking about the tools they trust when they head out on the course. From drivers to wedges to that one beat-up hybrid they swear by, every club has a role, and every bag tells a story.</p>
<p>Well, I think developers are a lot like golfers. We each carry a different set of tools. Some we've used for years and know inside and out. Others we’re testing out, seeing how they perform in different conditions. Our tools define our approach—and how we solve problems.</p>
<p>So I’m kicking off a new series on my blog called <strong>What’s In the Bag?</strong> In each post, I’ll explore a specific tech stack or framework, build something small but meaningful with it, and share my honest thoughts along the way. No hype. No gatekeeping. Just real-world experience with a touch of craft and curiosity.</p>
<hr />
<h3 id="heading-first-up-deno-fresh">First Up: Deno + Fresh</h3>
<p>The first stack in the bag is something I’ve been meaning to explore more seriously: <a target="_blank" href="https://deno.com/"><strong>Deno</strong></a> and the <a target="_blank" href="https://fresh.deno.dev/"><strong>Fresh</strong></a> web application framework.</p>
<p>For those unfamiliar, <strong>Deno</strong> is a modern JavaScript/TypeScript runtime created by the original creator of Node.js. It comes with built-in tooling (like testing and linting), strong security defaults, and native TypeScript support out of the box. <strong>Fresh</strong>, built on top of Deno, is a full-stack web framework focused on speed and simplicity. It uses "island architecture" to keep client-side JavaScript minimal by default, enabling ultra-fast performance and a better developer experience.</p>
<p>To test this stack, I’ll be building a <strong>collaborative to-do app</strong>—inspired by a showcase repo from the Deno team that uses <strong>server-sent events (SSE)</strong> to keep browser tabs in sync. The twist? Multiple users can view and update the same task list in real-time. No account system, no database (yet)—just a live playground for learning.</p>
<p>I'll cover:</p>
<ul>
<li><p>Project setup and routing in Fresh</p>
</li>
<li><p>How SSE works and why it's simpler than WebSockets for some use cases</p>
</li>
<li><p>Structuring shared state in a collaborative UI</p>
</li>
<li><p>What felt intuitive, what felt clunky, and what I’d do differently next time</p>
</li>
</ul>
<hr />
<h3 id="heading-why-this-matters">Why This Matters</h3>
<p>I’m doing this for a few reasons:</p>
<ul>
<li><p>To <strong>stay sharp</strong> by evaluating new tools</p>
</li>
<li><p>To <strong>create public artifacts</strong> I can point to</p>
</li>
<li><p>To <strong>rebuild the habit</strong> of sharing what I learn</p>
</li>
</ul>
<p>And let’s be honest—like any golfer trying out a new driver, sometimes it’s just fun to see what happens when you take a swing.</p>
<p>If you're curious about Deno, Fresh, or just like hearing how developers think through new stacks, I hope you’ll follow along.</p>
<hr />
<h3 id="heading-join-the-conversation">Join the Conversation</h3>
<p>Have you tried Deno or Fresh? Got a tech tool you think should be in the bag? I’d love to hear what you’re exploring or experimenting with. Drop a comment, tag me on LinkedIn, or share your “what’s in the bag” story.</p>
<p><strong>First build drops later this week.</strong></p>
<p>Thanks for being here.</p>
]]></content:encoded></item><item><title><![CDATA[Why I’m Writing Again: Golf, Code, and Finding My Third Act]]></title><description><![CDATA[Everything old is new again
For the last several years, I’ve been building software quietly behind the scenes. My focus was deep in the architecture, the tooling, and the day-to-day work of helping teams ship reliable code. And while that work has be...]]></description><link>https://shankproof.dev/why-im-writing-again-golf-code-and-finding-my-third-act</link><guid isPermaLink="true">https://shankproof.dev/why-im-writing-again-golf-code-and-finding-my-third-act</guid><category><![CDATA[Software Engineering]]></category><category><![CDATA[golf]]></category><category><![CDATA[golf technology]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Build In Public]]></category><category><![CDATA[Functional Programming]]></category><category><![CDATA[AI]]></category><category><![CDATA[devtools]]></category><dc:creator><![CDATA[Ryan Bell]]></dc:creator><pubDate>Wed, 02 Apr 2025 02:33:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1743560520351/abb56522-9ef7-4f19-a03e-da6d5221bf42.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-everything-old-is-new-again">Everything old is new again</h2>
<p>For the last several years, I’ve been building software quietly behind the scenes. My focus was deep in the architecture, the tooling, and the day-to-day work of helping teams ship reliable code. And while that work has been rewarding, I’ve come to realize I’ve missed something important: sharing what I’ve learned—and exploring what I <em>still</em> want to learn.</p>
<p>So here I am, writing again.</p>
<hr />
<h2 id="heading-a-little-background">A Little Background</h2>
<p>I’ve been a professional software engineer for about 30 years. I’ve built everything from Electronic Medical Records and public health systems to dev tooling and AI-enhanced educational platforms. I’ve worked in big teams, scrappy startups, and everything in between. I’ve led engineering orgs and built systems with 99.999% uptime. I’ve mentored junior devs, contributed to open source (shoutout to JRuby), and maintained libraries like Taskarian and Jsonous that reflect my love for functional programming.</p>
<p>But over time, I stopped writing about it. I got comfortable. Head down, shipping code, helping others get their work across the line.</p>
<p>And honestly, that was fine—until it wasn’t. Because I believe writing makes us sharper. Building in public keeps us honest. And articulating ideas—especially the imperfect, in-progress ones—is how we grow.</p>
<hr />
<h2 id="heading-why-now">Why Now?</h2>
<p>Lately I’ve felt the itch to connect again—to resuscitate those old blogging muscles, not just to teach, but to reflect. I want to document what I’m working on, share what I’m thinking about, and maybe even help a few people along the way.</p>
<p>But this time, I want to do something a little different.</p>
<p>This time, I want to bring golf into the mix.</p>
<hr />
<h2 id="heading-golf-code-really">Golf + Code? Really?</h2>
<p>Yeah. Really.</p>
<p>During COVID, I developed a deep love of golf. I’m no pro, but I’ve spent enough time on the course—and building tools <em>about</em> the course—to know there’s something fascinating in the overlap between software and the swing. Systems thinking. Feedback loops. Precision. Resets. Debugging under pressure.</p>
<p>And lately I’ve started building apps for golfers. Tools like Yardage Card and StrokeSheet—apps that help players understand their own game the way developers debug production systems. I’ll write about those projects here, along with the libraries, design decisions, and technical rabbit holes they lead me down.</p>
<hr />
<h2 id="heading-what-to-expect">What to Expect</h2>
<p>This blog will be a mix of:</p>
<ul>
<li><p>Real-world software engineering thoughts and practices</p>
</li>
<li><p>Functional programming and Typescript nerdery</p>
</li>
<li><p>Developer productivity tools (including AI-assisted workflows)</p>
</li>
<li><p>Golf tech, stats, and systems</p>
</li>
<li><p>Maybe the occasional metaphorical crossover—because debugging a smother hook feels a lot like debugging a bug</p>
</li>
</ul>
<p>If that sounds like your kind of thing, I hope you’ll stick around. I’m writing this for people who care about craft, who like to build, and who maybe—just maybe—believe that thoughtful systems can make everything a little more fun.</p>
<p>Thanks for being here. Let’s see where this goes.</p>
]]></content:encoded></item></channel></rss>