Add a Little Safety to your Elixir Structs with TypedStruct

<p>I&rsquo;m working on an Elixir app at the moment and really enjoying it, but some of my recent dabbling in the type-safe worlds of <a href="https://elm-lang.org">Elm</a> and <a href="https://crystal-lang.org">Crystal</a> have left me desiring a bit more structure in my code. The app I&rsquo;m building involves a multi-step data transformation and so I have a data structure to properly represent this process. But since Elixir is a dynamically typed language, you can&rsquo;t, for example, have a non-nillable field in a struct. The Elixir/Erlang ecosystem does, however, have a type-checking syntax called <a href="https://hexdocs.pm/elixir/typespecs.html">Type Specs</a>, along with a tool, <a href="http://erlang.org/doc/man/dialyzer.html">Dialyzer</a>, that will find a fair number of errors at compile-time.</p> <p>But I found that using <code>defstruct</code> with type specs to be fairly verbose and unintuitive. For example, let&rsquo;s say you want a &ldquo;Person&rdquo; struct with the following attributes:</p> <ul> <li>Name - string (non-nillable)</li> <li>Age - positive integer or nil</li> <li>Happy? - boolean (default to true)</li> <li>Phone - string or nil</li> </ul> <p>Normally you&rsquo;d do the following:</p> <div class="highlight"><pre class="highlight elixir"><code><span class="k">defmodule</span> <span class="no">Person</span> <span class="k">do</span> <span class="nv">@enforce_keys</span> <span class="p">[</span><span class="ss">:name</span><span class="p">]</span> <span class="k">defstruct</span> <span class="ss">name:</span> <span class="no">nil</span><span class="p">,</span> <span class="ss">age:</span> <span class="no">nil</span><span class="p">,</span> <span class="ss">happy?:</span> <span class="no">true</span><span class="p">,</span> <span class="ss">phone:</span> <span class="no">nil</span> <span class="nv">@type</span> <span class="n">t</span><span class="p">()</span> <span class="p">::</span> <span class="p">%</span><span class="bp">__MODULE__</span><span class="p">{</span> <span class="ss">name:</span> <span class="no">String</span><span class="o">.</span><span class="n">t</span><span class="p">(),</span> <span class="ss">age:</span> <span class="n">non_neg_integer</span><span class="p">()</span> <span class="o">|</span> <span class="no">nil</span><span class="p">,</span> <span class="ss">happy?:</span> <span class="n">boolean</span><span class="p">(),</span> <span class="ss">phone:</span> <span class="no">String</span><span class="o">.</span><span class="n">t</span><span class="p">()</span> <span class="o">|</span> <span class="no">nil</span> <span class="p">}</span> <span class="k">end</span> </code></pre></div> <p>As you can see, the names of the keys and defaults are separate from the type information. This can get pretty hairy if you have a large, complex data structure and is very unintuitive if you aren&rsquo;t already familiar with type specs. Luckily, I stumbled upon the <a href="https://github.com/ejpcmac/typed_struct">TypedStruct</a> package, which is a compile-time macro that dramatically improves the syntax. Using TypedStruct, the above example would look like this:</p> <div class="highlight"><pre class="highlight elixir"><code><span class="k">defmodule</span> <span class="no">Person</span> <span class="k">do</span> <span class="kn">use</span> <span class="no">TypedStruct</span> <span class="n">typedstruct</span> <span class="k">do</span> <span class="n">field</span> <span class="ss">:name</span><span class="p">,</span> <span class="no">String</span><span class="o">.</span><span class="n">t</span><span class="p">(),</span> <span class="ss">enforce:</span> <span class="no">true</span> <span class="n">field</span> <span class="ss">:age</span><span class="p">,</span> <span class="n">non_neg_integer</span><span class="p">()</span> <span class="n">field</span> <span class="ss">:happy?</span><span class="p">,</span> <span class="n">boolean</span><span class="p">(),</span> <span class="ss">default:</span> <span class="no">true</span> <span class="n">field</span> <span class="ss">:phone</span><span class="p">,</span> <span class="no">String</span><span class="o">.</span><span class="n">t</span><span class="p">()</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <p>Now we&rsquo;re talking! This is immediately understandable to anyone coming across it, particularly if you&rsquo;re used to the DSL used to define schemas in <a href="https://hexdocs.pm/ecto/Ecto.html">Ecto</a>.</p> <p>So then what happens when I use the struct improperly?</p> <div class="highlight"><pre class="highlight elixir"><code><span class="k">defmodule</span> <span class="no">PersonUser</span> <span class="k">do</span> <span class="nv">@spec</span> <span class="n">init_person</span><span class="p">()</span> <span class="p">::</span> <span class="no">Person</span><span class="o">.</span><span class="n">t</span><span class="p">()</span> <span class="k">def</span> <span class="n">init_person</span> <span class="k">do</span> <span class="p">%</span><span class="no">Person</span><span class="p">{</span><span class="ss">name:</span> <span class="no">nil</span><span class="p">,</span> <span class="ss">age:</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="ss">happy?:</span> <span class="s2">"yes"</span><span class="p">}</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <p>NeoVim, configured properly with <a href="https://github.com/neoclide/coc.nvim">Coc.nvim</a>, gives me a popup warning message:</p> <p><img src="/assets/images/elixir_dialyzer_vim_typed_struct-f7708aa9.png" alt="NeoVim Elixir Typespec Warning Message" /></p> <p>And VSCode, with the <a href="https://marketplace.visualstudio.com/items?itemName=JakeBecker.elixir-ls">Elixir-LS</a> extension, will do the same:</p> <p><img src="/assets/images/elixir_dialyzer_vscode_typed_struct-f3577a06.png" alt="VSCode Elixir Typespec Warning Message" /></p> <p>In an ideal world, the program wouldn&rsquo;t compile. But this is still a huge help, particularly in a large codebase that you&rsquo;re not completely familiar with.</p>