Typed Elixir structs without boilerplate
This article was originally published on Medium.
TD;DR A package is available on hex.pm and GitHub.
In Elixir, you can define a struct by calling defstruct
in a module. This
macro takes a list of atoms which becomes the keys of the struct, or a keyword
list associating these keys to default values:
defstruct name: "John Smith",
age: nil,
phone: nil
All the keys are optional by default. To enforce some of them, you must add them
to @enforce_keys
:
@enforce_keys [:age]
The official documentation recommends to define a type for the
struct, named
t()
by convention:
@type t() :: %__MODULE__{
name: String.t(),
age: integer(),
phone: String.t() | nil
}
Since :phone
is not enforced nor has a default value, it can indeed be nil
,
so I have made the type nullable here.
Wrapping all together, it takes some code to get a struct with some enforced keys and with a type:
defmodule Example do
@enforce_keys [:age]
defstruct name: "John Smith",
age: nil,
phone: nil
@type t() :: %__MODULE__{
name: String.t(),
age: integer(),
phone: String.t() | nil
}
end
The keys are repeated in several places. To add a new enforced key, you must add it in three places:
defmodule Example do
@enforce_keys [:age, :email] # Add :email here
defstruct name: "John Smith",
age: nil,
email: nil, # Here too
phone: nil
@type t() :: %__MODULE__{
name: String.t(),
age: integer(),
email: String.t(), # And here
phone: String.t() | nil
}
end
This is quite error-prone and also too much work for lazy people. To avoid repeating myself, I started to write some awkward things like this:
defmodule Example do
@enforce_keys [:age]
@fields quote(
do: [
name: String.t(),
age: integer(),
email: String.t() | nil
]
)
defstruct Keyword.keys(@fields)
@type t() :: %__MODULE__{unquote_splicing(@fields)}
end
With this pattern, adding a key is a bit less work: you just have to add it to
the quoted block, and in the @enforce_keys
if needed. Yet, there is still
repeat, default values are not handled and the code is really awkward. Using
this pattern more than twice led me to think of playing with Elixir macros to
get something cleaner. I’ve come up with TypedStruct:
defmodule Example do
use TypedStruct
typedstruct do
field :name, String.t(), default: "John Smith"
field :age, integer(), enforce: true
field :email, String.t()
end
end
With this Ecto-inspired API, you can define both the struct, the type and
@enforce_keys
at once. The default value is another option, and adding a new
key is a breeze. It is available on
hex.pm and
GitHub, so feel free to use it in
your projects. You can read the
documentation for more
accurate information. If you find something strange or have some ideas to share,
don’t hesitate to reply to this story or open an issue on GitHub. This being
shared, have a nice day!