Typespecs, Dialyzer & the Type System

Common types: any(), term(), atom(), boolean(), integer(), pos_integer(), non_neg_integer(), float(), number(), String.t(), binary(), list(t), [t], nonempty_list(t), keyword(), keyword(t), map(), %{key: t}, %{optional(k) => v}, %User{}, tuple(), {t1, t2}, f…

23.1 Typespecs

Common types: any(), term(), atom(), boolean(), integer(), pos_integer(), non_neg_integer(), float(), number(), String.t(), binary(), list(t), [t], nonempty_list(t), keyword(), keyword(t), map(), %{key: t}, %{optional(k) => v}, %User{}, tuple(), {t1, t2}, fun(), (a -> b), pid(), reference(), mfa(), module(), timeout(), no_return(), nil, unions a | b, ranges 1..10, literals :ok.

Struct type convention:

defmodule Shapes do
  @type shape :: {:circle, radius :: number()} | {:rect, number(), number()}
  @typep internal :: map()
  @opaque handle :: reference()

  @spec area(shape()) :: float()
  def area({:circle, r}), do: :math.pi() * r * r
  def area({:rect, w, h}), do: w * h * 1.0
end
@type t :: %__MODULE__{name: String.t(), age: non_neg_integer()}

23.2 Dialyzer

Static analysis via success typings:

# mix.exs deps
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}
mix dialyzer

23.3 The built-in gradual type system (1.17+)

Elixir is gaining a set-theoretic gradual type system. Since 1.17 the compiler infers types from patterns and guards and warns about obvious type violations (e.g., calling a function on a struct field that doesn't exist, comparing incompatible types, impossible clauses). 1.18 extended checking to function calls and protocol dispatch; later versions add inference across function boundaries. No annotations are required yet — future versions will add type signatures.

def example(%Date{} = d), do: d.hour   # compiler warning: Date has no :hour field