Typed Elixir structs without boilerplate

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!