Types and Specs

Elixir basic types, type specs, and dialyzer for static type analysis

Basic types

Type Example Notes
Integer 1, 0xFF, 0b1010, 0o777 Arbitrary precision
Float 3.14, 1.0e10 IEEE 754 double
Boolean true, false Atoms underneath
Atom :ok, :error, :hello Interned strings
String "hello" UTF-8 binary
Charlist 'hello' List of codepoints
List [1, :a, "hi"] Linked list
Tuple {1, :ok} Fixed-size, indexed
Map %{a: 1} Key-value
Function fn x -> x end Anonymous / named
PID self() Process identifier
Reference make_ref() Unique reference
Nil nil Atom, represents null

Type checks

is_atom(:ok)        # => true
is_boolean(true)    # => true
is_integer(1)       # => true
is_float(3.14)      # => true
is_number(1)        # => true (int or float)
is_binary("hi")    # => true
is_list([1, 2])    # => true
is_tuple({1, 2})   # => true
is_map(%{a: 1})    # => true
is_function(fn -> end)  # => true
is_pid(self())     # => true
is_nil(nil)        # => true

Type specifications

Elixir supports optional type annotations via @type, @typep, @opaque, @spec, and @callback.

Defining types

defmodule User do
  @type t :: %__MODULE__{
    name: String.t(),
    age: non_neg_integer(),
    email: String.t() | nil
  }

  @type id :: pos_integer()

  defstruct [:name, :age, :email]
end

Common built-in types

Type Meaning
integer() Any integer
float() Any float
number() Integer or float
boolean() true or false
atom() Any atom
String.t() UTF-8 string (binary)
binary() Any binary
list(any()) A list
[type()] A list of type
map() Any map
%{key: type} Map with specific keys
tuple() Any tuple
{type1, type2} 2-tuple with specific types
pid() Process identifier
reference() Unique reference
any() Any value
none() No value (unreachable)
iodata() binary() | list()
charlist() list(integer())
non_neg_integer() Integer ≥ 0
pos_integer() Integer > 0
neg_integer() Integer < 0

Union and nullable types

@type status :: :ok | :error | :pending
@type maybe_string :: String.t() | nil
@type result :: {:ok, term()} | {:error, String.t()}

Remote types

@type uri :: URI.t()
@type date :: Date.t()

Function specs

defmodule Math do
  @spec add(number(), number()) :: number()
  def add(a, b), do: a + b

  @spec divide(number(), number()) :: {:ok, float()} | {:error, String.t()}
  def divide(_, 0), do: {:error, "division by zero"}
  def divide(a, b), do: {:ok, a / b}

  @spec find_user(pos_integer()) :: User.t() | nil
  def find_user(id), do: Repo.get(User, id)
end

Callback specs (behaviours)

defmodule Parser do
  @callback parse(String.t()) :: {:ok, term()} | {:error, String.t()}
  @callback extensions() :: [String.t()]
end

Type visibility

Declaration Visibility
@type Public — can be used by other modules
@typep Private — only within the module
@opaque Public name, private structure — callers can’t see internals
defmodule SecretBox do
  @opaque t :: %__MODULE__{value: term()}
  defstruct [:value]
end

Dialyzer

Dialyzer is a static analysis tool that identifies type discrepancies.

# Install dialyxir
mix deps.add dialyxir

# Build the PLT (persistent lookup table) — first time only
mix dialyzer --plt

# Run analysis
mix dialyzer

# Run with helpful explanations
mix dialyzer --format explain

Gradual type checking with gradualizer

For stricter type checking, consider Gradualizer or the upcoming Elixir set-theoretic types (Elixir 1.17+).

Elixir 1.17+ pattern-based types

Elixir 1.17 introduced set-theoretic type checking:

# Elixir 1.17+ can infer and warn about type mismatches
def example do
  x = if true, do: 1, else: :ok  # x is integer() | atom()
  String.length(x)  # warning: x may not be a string
end