Enforcing type constraints

I would like to impose a constraint on my types in Elixir. I am trying to do this with the @type directive.

defmodule Thing do
  @enforce_keys [:foo, :bar]

  defstruct [:foo, :bar]

  @type t :: %__MODULE__{
          foo: String.t(),
          bar: integer()
        }
end

I would like to constrain Thing structs to something like {foo :: string, bar :: int}, but this seems to pass through just fine:

iex(39)> obj = %Thing{foo: "foo", bar: "bar"}
%Thing{bar: "bar", foo: "foo"}
iex(40)> obj
%Thing{bar: "bar", foo: "foo"}

Is there an easy way to impose type constraints to prevent this from happening?

Yes, harness pattern matching for your needs at runtime. Typespecs only help during compile time – and they are voluntary; code will still compile even if they are not obeyed.

defmodule Thing do
  # ...your original code from above is here...

  def new(foo, bar) when is_binary(foo) and is_integer(bar) do
    %__MODULE__{foo: foo, bar: bar}
  end
end

…then just go like this:

iex> Thing.new("foo", "bar")
# this will raise an error
3 Likes

Elixir doesn’t have a static type system enforced by the compiler - there is a separate tool that you use for analysis using these types called dialyzer. While the runtime does have a strong type system, it doesn’t know about the @type or @spec annotations that you make.

@dimitarvp’s answer is the best one - the pattern match constraints will also inform the success typing analysis of dialyzer - so you will get the benefit of type requirements statically and at runtime. If you want to get started using dialyzer with Elixir I recommend this mix task that I maintain.

6 Likes