Introduction
Elixir is dynamically typed, and a lot of its appeal comes from that: no ceremony to define a function, fast feedback, expressive code.
But “typing in Elixir” is a bigger topic than it used to be. Today it covers three distinct things:
- the runtime shape you get for free from pattern matching and structs,
- optional typespecs, checked by Dialyzer,
- and the set-theoretic type system now built into the compiler.
Let’s look at each.
Dynamically typed, not untyped
Elixir has types; it just doesn’t check them at compile time by default. You work with them constantly:
"hello" # a binary (string)
:ok # an atom
123 # an integer
%{key: "value"} # a map
[1, 2, 3] # a list
%User{name: "nico"} # a struct
Instead of a type checker, you lean on pattern matching, structs, and runtime errors when shapes don’t line up. For a lot of code, that’s enough.
Runtime shape: matching, guards, structs
Pattern matching enforces expectations without any type annotation. This
function only accepts a %User{}, and raises on anything else:
def greet(%User{name: name}) do
"Hello, #{name}!"
end
Guards add finer conditions:
def double(x) when is_integer(x), do: x * 2
And structs give data a fixed, self-documenting shape:
defmodule User do
defstruct [:name, :email]
end
None of this is compile-time checking, but it’s real structure, enforced when the code runs.
Typespecs
When you want to document contracts (a public API, a complex return type),
you annotate functions with @spec:
@spec fetch_user(integer()) :: {:ok, User.t()} | {:error, :not_found}
def fetch_user(id) do
case Repo.get(User, id) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end
You can name your own types, too:
@type status :: :active | :inactive | :banned
Typespecs are worth it on public functions and non-obvious return types, and they help editors and language servers. On short, obvious helpers they’re just noise.
Dialyzer
Typespecs are checked by Dialyzer. To be precise about it, Dialyzer is an
Erlang/OTP tool, used from Elixir through the dialyxir library. It’s a
success typing analyser: it doesn’t force you to write specs and doesn’t
block compilation; it flags code it can prove is inconsistent. It’s useful,
but it won’t catch everything, and it’s a separate step you run.
The built-in type system
The bigger news is that Elixir now has its own type system, built into the compiler. It started in Elixir 1.17 (2024) and has grown with each release since.
Right now it works by inference: the compiler infers types for your code
and emits a warning when it’s certain a piece of code will fail, with no
annotations required, and the checking crosses module and dependency
boundaries. For example, passing a value that could be atom() or integer()
into Integer.to_string/1 warns, because an atom isn’t accepted there.
There’s no user-facing type syntax yet. The roadmap is public and staged: inference first (where we are), then a way to define typed structs, then set-theoretic type signatures for functions, at which point the old Erlang-style typespecs are expected to be phased out. The system is designed to be sound and gradual, which is a different foundation from Dialyzer’s.
In practice that means you already get some compile-time type checking today, for free, just by upgrading Elixir, and it will only get stronger.
What to use, and when
- Lean on pattern matching and structs as your first line of structure.
- Add
@specto public functions and anything with a non-obvious return type; skip it on trivial helpers. - Run Dialyzer if you want deeper analysis of those specs, knowing it’s a help, not a guarantee.
- Keep Elixir current to benefit from the compiler’s growing type checker without changing a line of your code.
Wrapping up
Typing in Elixir used to mean “typespecs and Dialyzer, if you bother.” That’s no longer the whole story: the language is quietly gaining a real, sound type system that meets you where dynamic code already is. You still choose how much to annotate, but you now get a baseline of compile-time checking for nothing, and the direction of travel is clearly toward more.
Have comments or want to discuss this topic?
Send an email to ~bounga/bounga.org-discuss@lists.sr.ht