I’m working on an Elixir app at the moment and really enjoying it, but some of my recent dabbling in the type-safe worlds of Elm and Crystal have left me desiring a bit more structure in my code. The app I’m building involves a multi-step data transformation and so I have a data structure to properly represent this process. But since Elixir is a dynamically typed language, you can’t, for example, have a non-nillable field in a struct. The Elixir/Erlang ecosystem does, however, have a type-checking syntax called Type Specs, along with a tool, Dialyzer, that will find a fair number of errors at compile-time.
But I found that using defstruct
with type specs to be fairly verbose and unintuitive. For example, let’s say you want a “Person” struct with the following attributes:
- Name - string (non-nillable)
- Age - positive integer or nil
- Happy? - boolean (default to true)
- Phone - string or nil
Normally you’d do the following:
defmodule Person do
@enforce_keys [:name]
defstruct name: nil,
age: nil,
happy?: true,
phone: nil
@type t() :: %__MODULE__{
name: String.t(),
age: non_neg_integer() | nil,
happy?: boolean(),
phone: String.t() | nil
}
end
As you can see, the names of the keys and defaults are separate from the type information. This can get pretty hairy if you have a large, complex data structure and is very unintuitive if you aren’t already familiar with type specs. Luckily, I stumbled upon the TypedStruct package, which is a compile-time macro that dramatically improves the syntax. Using TypedStruct, the above example would look like this:
defmodule Person do
use TypedStruct
typedstruct do
field :name, String.t(), enforce: true
field :age, non_neg_integer()
field :happy?, boolean(), default: true
field :phone, String.t()
end
end
Now we’re talking! This is immediately understandable to anyone coming across it, particularly if you’re used to the DSL used to define schemas in Ecto.
So then what happens when I use the struct improperly?
defmodule PersonUser do
@spec init_person() :: Person.t()
def init_person do
%Person{name: nil, age: -1, happy?: "yes"}
end
end
NeoVim, configured properly with Coc.nvim, gives me a popup warning message:
And VSCode, with the Elixir-LS extension, will do the same:
In an ideal world, the program wouldn’t compile. But this is still a huge help, particularly in a large codebase that you’re not completely familiar with.