<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: GeekMasahiro</title>
    <description>The latest articles on DEV Community by GeekMasahiro (@geekmasahiro).</description>
    <link>https://dev.to/geekmasahiro</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3993529%2Fa0622c21-42f3-471d-811d-5661e8849155.png</url>
      <title>DEV Community: GeekMasahiro</title>
      <link>https://dev.to/geekmasahiro</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/geekmasahiro"/>
    <language>en</language>
    <item>
      <title>Elixir 1.20 has a type system now: comparing it with Rust and TypeScript</title>
      <dc:creator>GeekMasahiro</dc:creator>
      <pubDate>Sat, 20 Jun 2026 12:12:16 +0000</pubDate>
      <link>https://dev.to/geekmasahiro/elixir-120-has-a-type-system-now-comparing-it-with-rust-and-typescript-31p4</link>
      <guid>https://dev.to/geekmasahiro/elixir-120-has-a-type-system-now-comparing-it-with-rust-and-typescript-31p4</guid>
      <description>&lt;p&gt;&lt;em&gt;This English version is an AI translation of &lt;a href="https://qiita.com/GeekMasahiro/items/3f8e66db661f8a2f97a5" rel="noopener noreferrer"&gt;my original article on Qiita (in Japanese)&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  About this article
&lt;/h2&gt;

&lt;p&gt;On June 3, 2026, Elixir 1.20 shipped with a type system. But it aims at something a little different from the type systems we are used to, the ones where you write types to buy safety.&lt;/p&gt;

&lt;p&gt;This article started from my own questions, which I worked through with an AI agent. We went to primary sources (the official blog, the docs, the paper), and in the end I installed Elixir 1.20 and checked how the type checker actually behaves. I may still have gotten things wrong, so if you notice anything, I would be glad to hear it in the comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the type system aims for: soundness
&lt;/h2&gt;

&lt;p&gt;First, the word &lt;strong&gt;sound&lt;/strong&gt;. In type systems, a type system is &lt;em&gt;sound&lt;/em&gt; when, if it says a value has some type, you will not get a type error of that kind at runtime. The opposite, &lt;em&gt;unsound&lt;/em&gt;, means a program can type-check and still hit a type error when it runs.&lt;/p&gt;

&lt;p&gt;Making a dynamic language fully sound is hard. TypeScript openly states that it is &lt;strong&gt;not&lt;/strong&gt; sound (it lists "apply a sound or 'provably correct' type system" as a non-goal). Elixir, on the other hand, lists soundness as one of the goals of its type system. But it too admits it is best-effort ("we do not guarantee we will find every mistake"), so it does not claim to be fully sound either. Both projects talk about their type systems in terms of sound and unsound, so I will use the same words here.&lt;/p&gt;

&lt;p&gt;There is room for different stances on how far, and how, to pursue soundness. The key question is &lt;strong&gt;how each one treats things it cannot be sure about&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Sound?&lt;/th&gt;
&lt;th&gt;Unknowns are...&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rust&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;sound (strong)&lt;/td&gt;
&lt;td&gt;rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Elixir&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;sound (best-effort)&lt;/td&gt;
&lt;td&gt;passed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TypeScript&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;unsound&lt;/td&gt;
&lt;td&gt;passed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rust&lt;/strong&gt; rejects what it cannot be sure about. Because it refuses to let those through, it is strongly sound about types and ownership. (The &lt;code&gt;unsafe&lt;/code&gt; parts, and runtime errors like out-of-bounds array access, are outside that guarantee.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript&lt;/strong&gt; and &lt;strong&gt;Elixir&lt;/strong&gt; both let unknowns through in the end. The difference is what happens before that. TypeScript is designed around writing types as much as you can, and where type information is missing (an &lt;code&gt;any&lt;/code&gt;, or something made unknown by an &lt;code&gt;as&lt;/code&gt; cast) it treats that as out of scope and lets it through without checking. Elixir, before letting a value through, traces the flow of the code (branches and guards) to work out the possible types, and rejects only what it can prove will always fail. Same "letting it through," but the question is &lt;strong&gt;whether it looks first&lt;/strong&gt;. That difference is what separates unsound (TypeScript) from sound (Elixir).&lt;/p&gt;

&lt;h2&gt;
  
  
  Elixir 1.20's type system
&lt;/h2&gt;

&lt;p&gt;Three things stand out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;dynamic()&lt;/code&gt; defers the judgment.&lt;/strong&gt; Where type information is missing, TypeScript passes it without checking (by design: it assumes you write types, and treats what you did not as out of scope). Elixir instead assigns the unknown spot a type that literally means "unknown" (&lt;code&gt;dynamic()&lt;/code&gt;) and holds off. As that value flows through the code it gets narrowed, and Elixir reports only the spots where every possible value is certain to fail. Where only some paths might fail, it stays quiet. It is built to say only what it is sure of, and to avoid false positives. If something stays gray to the end it is missed (the project itself admits this best-effort limit), but whatever it does report is certain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. You can express negation in a type.&lt;/strong&gt; Elixir's types are built from set operations: union, intersection, and negation (set-theoretic types). So "not nil" is natural to write. TypeScript can also say "exclude this" via &lt;code&gt;Exclude&lt;/code&gt; and narrowing, but Elixir has negation as a type operation itself. That is the difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It starts from writing no types.&lt;/strong&gt; For now, you do not write type annotations; the compiler infers types from the code and checks them. The point is to make it work on huge existing codebases without adding anything. Later, the plan is to let you write type signatures at the boundaries where you want stronger guarantees (per the official roadmap), and 1.20 is the "inference first" milestone on that path. Note that the long-standing &lt;code&gt;@spec&lt;/code&gt; is for documentation and Dialyzer; the new type system does not use it. The new type signatures are being designed as a separate thing.&lt;/p&gt;

&lt;p&gt;Once you can write types explicitly, guarantees should reach places inference cannot follow today (struct fields, boundaries across modules). But the team itself is cautious ("we still need to assess the impact on how it feels to write code" and "it may turn out the type system is not practical"), and it is still being evaluated. The future where you write types should be better, but the people building it are still feeling their way.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much it catches without a single type annotation (I ran it)
&lt;/h2&gt;

&lt;p&gt;I installed Elixir 1.20 and tried, without writing a single type, to see what it catches.&lt;/p&gt;

&lt;p&gt;For example, when every branch of a &lt;code&gt;case&lt;/code&gt; returns an atom, the result is inferred to be an atom, and things stop when you hand that to a function that works on strings. Elixir calls strings "binaries," so the warning says &lt;code&gt;binary()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;categorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;when&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:negative&lt;/span&gt;
      &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:zero&lt;/span&gt;
      &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:positive&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;warning: incompatible types given to String.length/1:
    given types:     dynamic(:negative or :positive or :zero)
    but expected one of:     binary()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also follows types that change midway through a pipeline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;n&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# turn the integer into a string (Elixir calls strings "binaries")&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Kernel&lt;/span&gt;&lt;span class="o"&gt;.+&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# warns: trying to add an integer to a string (binary)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;warning: incompatible types given to Kernel.+/2:
    given types:     binary(), integer()
    but expected one of:     integer(), integer()   # and the other numeric combinations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither has any type annotations. It bundles the three &lt;code&gt;case&lt;/code&gt; branches into &lt;code&gt;:negative or :positive or :zero&lt;/code&gt;, follows the type change through the pipe, and points at the single spot where every value is bound to fail.&lt;/p&gt;

&lt;p&gt;To be honest, I tried to build an example where &lt;strong&gt;TypeScript misses it but only Elixir catches it&lt;/strong&gt;, and I could not make one work. TypeScript catches type mismatches too, and unsound cases like out-of-bounds array access are missed by Elixir as well (best-effort). The difference is not the range of what can be caught; it is that &lt;strong&gt;it catches this without my writing a single type for the example&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pushed further: wouldn't a statically typed language be enough?
&lt;/h2&gt;

&lt;p&gt;After running all this, a deeper question surfaced. If we have statically typed languages like Rust, then pushed to the limit, wouldn't those be enough? Why do dynamically typed languages need to exist at all? I wanted to find out, so I asked an AI agent. Here is the answer that came back.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;First, let me split the question a little. Rust's heaviness comes less from static types themselves and more from ownership and lifetimes (the machinery for memory safety without a GC). A statically typed language with a GC gets type inference too, so you barely write annotations. So the real divide is not "Rust or dynamic" but "static or dynamic," with GC'd static languages sitting in between.&lt;/p&gt;

&lt;p&gt;To be honest, the "static is enough" side has gotten stronger. Inference has narrowed the old "dynamic is faster to write" gap, and the trend of adding types to a dynamic language after the fact (TypeScript, Python, and now Elixir 1.20) is itself evidence that once code matters, people end up wanting static guarantees.&lt;/p&gt;

&lt;p&gt;Even so, there are two reasons dynamic types will not disappear in principle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A sound static type system must reject some programs that actually run correctly.&lt;/strong&gt; Whether a program "never hits a type error at runtime" is undecidable in general (&lt;a href="https://en.wikipedia.org/wiki/Rice%27s_theorem" rel="noopener noreferrer"&gt;Rice's theorem&lt;/a&gt;), so if you prioritize never letting an unsafe program through, the price is that some "actually runs, but cannot be proven, so it is rejected" code always remains. This is not a flaw you fix by improving the type system; it is a hard ceiling. The proof is that every static language has an escape hatch (Rust's &lt;code&gt;unsafe&lt;/code&gt;, the &lt;code&gt;Any&lt;/code&gt; or dynamic casts in various languages). They admit there is a part static cannot cover, and quietly bring dynamic typing back in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Some work has types that are only determined at runtime.&lt;/strong&gt; Messy data coming from outside, a REPL you poke at interactively, macros that generate code, hot code reloading that swaps modules while the system runs. All of these are dynamic by nature, and clash with the premise of "check the whole thing up front."&lt;/p&gt;

&lt;p&gt;In short, static and dynamic just differ in &lt;strong&gt;where you pay for type correctness&lt;/strong&gt;. Static pays up front, at compile time, for every possible execution. Dynamic pays later, at runtime, for the one execution that actually happened. Because there are situations where you cannot pay up front, or where paying up front is not worth it, dynamic typing is needed. And the real answer is not "one or the other" but is heading toward "keep a dynamic base, and add static where it pays."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reading that answer, I came to understand Elixir 1.20's reason for being, and its position, a little better.&lt;/p&gt;

&lt;p&gt;Elixir is not trying to become Rust. It keeps a base you can run while still dynamic, statically stops only the certain mistakes, and plans to let you add types at the boundaries where you want guarantees later. It has chosen "a dynamic base, with static added where it pays" as a matter of language design.&lt;/p&gt;

&lt;p&gt;And this overlaps with the era of having AI write code. AI can write a lot, fast, but it does not get everything completely right. So the type system points out, at the compiler level, the mistakes that are certain to be found, and assists with the parts AI does not quite finish. Elixir 1.20's type system looks like one form of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://elixir-lang.org/blog/2026/06/03/elixir-v1-20-0-released/" rel="noopener noreferrer"&gt;Elixir v1.20 released: now a gradually typed language&lt;/a&gt; (official release blog)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hexdocs.pm/elixir/gradual-set-theoretic-types.html" rel="noopener noreferrer"&gt;Gradual set-theoretic types&lt;/a&gt; (official docs: the note on soundness, best-effort, and phasing out typespecs)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hexdocs.pm/elixir/typespecs.html" rel="noopener noreferrer"&gt;Typespecs reference&lt;/a&gt; (&lt;code&gt;@spec&lt;/code&gt; is for documentation and Dialyzer)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://elixir-lang.org/blog/2026/01/09/type-inference-of-all-and-next-15/" rel="noopener noreferrer"&gt;Type inference of all constructs and the next 15 months&lt;/a&gt; (roadmap)&lt;/li&gt;
&lt;li&gt;Giuseppe Castagna, Guillaume Duboc, José Valim, &lt;a href="https://arxiv.org/pdf/2306.06391" rel="noopener noreferrer"&gt;"The Design Principles of the Elixir Type System"&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/microsoft/TypeScript/wiki/TypeScript-Design-Goals" rel="noopener noreferrer"&gt;TypeScript Design Goals&lt;/a&gt; (lists a sound type system as a non-goal)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>elixir</category>
      <category>rust</category>
      <category>typescript</category>
      <category>types</category>
    </item>
  </channel>
</rss>
