TypedStructor is a library for defining structs with types effortlessly.
(This library is a rewritten version of TypedStruct because it is no longer actively maintained.)
Add :typed_structor to the list of dependencies in mix.exs:
def deps do
[
{:typed_structor, "~> 0.5"}
]
endAdd :typed_structor to your .formatter.exs file
[
# import the formatter rules from `:typed_structor`
import_deps: [..., :typed_structor],
inputs: [...]
]To define a struct with types, use TypedStructor,
and then define fields under the TypedStructor.typed_structor/2 macro,
using the TypedStructor.field/3 macro to define each field.
defmodule User do
# use TypedStructor to import the `typed_structor` macro
use TypedStructor
typed_structor do
# Define each field with the `field` macro.
field :id, pos_integer()
# set a default value
field :name, String.t(), default: "Unknown"
# enforce a field
field :age, non_neg_integer(), enforce: true
end
endThis is equivalent to:
defmodule User do
defstruct [:id, :name, :age]
@type t() :: %__MODULE__{
id: pos_integer() | nil,
# Note: The 'name' can not be nil, for it has a default value.
name: String.t(),
age: non_neg_integer()
}
endCheck TypedStructor.typed_structor/2 and TypedStructor.field/3 for more information.
You can also generate an opaque type for the struct,
even changing the type name:
defmodule User do
use TypedStructor
typed_structor type_kind: :opaque, type_name: :profile do
field :id, pos_integer()
field :name, String.t()
field :age, non_neg_integer()
end
endThis is equivalent to:
defmodule User do
use TypedStructor
defstruct [:id, :name, :age]
@opaque profile() :: %__MODULE__{
id: pos_integer() | nil,
name: String.t() | nil,
age: non_neg_integer() | nil
}
endType parameters also can be defined:
defmodule User do
use TypedStructor
typed_structor do
parameter :id
parameter :name
field :id, id
field :name, name
field :age, non_neg_integer()
end
endbecomes:
defmodule User do
@type t(id, name) :: %__MODULE__{
id: id | nil,
name: name | nil,
age: non_neg_integer() | nil
}
defstruct [:id, :name, :age]
endIf you prefer to define a struct in a submodule, you can use
the module option with TypedStructor. This allows you to
encapsulate the struct definition within a specific submodule context.
Consider this example:
defmodule User do
use TypedStructor
# `%User.Profile{}` is generated
typed_structor module: Profile do
field :id, pos_integer()
field :name, String.t()
field :age, non_neg_integer()
end
endWhen defining a struct in a submodule, the typed_structor block
functions similarly to a defmodule block. Therefore,
the previous example can be alternatively written as:
defmodule User do
defmodule Profile do
use TypedStructor
typed_structor do
field :id, pos_integer()
field :name, String.t()
field :age, non_neg_integer()
end
end
endFurthermore, the typed_structor block allows you to
define functions, derive protocols, and more, just
as you would within a defmodule block. Here's a example:
defmodule User do
use TypedStructor
typed_structor module: Profile, define_struct: false do
@derive {Jason.Encoder, only: [:email]}
field :email, String.t()
use Ecto.Schema
@primary_key false
schema "users" do
Ecto.Schema.field(:email, :string)
end
import Ecto.Changeset
def changeset(%__MODULE__{} = user, attrs) do
user
|> cast(attrs, [:email])
|> validate_required([:email])
end
end
endNow, you can interact with these structures:
iex> User.Profile.__struct__()
%User.Profile{__meta__: #Ecto.Schema.Metadata<:built, "users">, email: nil}
iex> Jason.encode!(%User.Profile{})
"{\"email\":null}"
iex> User.Profile.changeset(%User.Profile{}, %{"email" => "[email protected]"})
#Ecto.Changeset<
action: nil,
changes: %{email: "[email protected]"},
errors: [],
data: #User.Profile<>,
valid?: true
>In Elixir, an exception is defined as a struct that includes a special field named __exception__.
To define an exception, use the defexception definer within the typed_structor block.
defmodule HTTPException do
use TypedStructor
typed_structor definer: :defexception, enforce: true do
field :status, non_neg_integer()
end
@impl Exception
def message(%__MODULE__{status: status}) do
"HTTP status #{status}"
end
endIn Elixir, you can use the Record module to define and work with Erlang records, making interoperability between Elixir and Erlang more seamless.
defmodule TypedStructor.User do
use TypedStructor
typed_structor definer: :defrecord, record_name: :user, record_tag: User, enforce: true do
field :name, String.t()
field :age, pos_integer()
end
endTo add a @typedoc to the struct type, just add the attribute in the typed_structor block:
typed_structor do
@typedoc "A typed user"
field :id, pos_integer()
field :name, String.t()
field :age, non_neg_integer()
endYou can also document submodules this way:
typedstructor module: Profile do
@moduledoc "A user profile struct"
@typedoc "A typed user profile"
field :id, pos_integer()
field :name, String.t()
field :age, non_neg_integer()
endTypedStructor offers a plugin system to enhance functionality.
For details on creating a plugin, refer to the TypedStructor.Plugin module.
Here is a example of Guides.Plugins.Accessible plugin to define Access behavior for the struct.
defmodule User do
use TypedStructor
typed_structor do
plugin Guides.Plugins.Accessible
field :id, pos_integer()
field :name, String.t()
field :age, non_neg_integer()
end
end
user = %User{id: 1, name: "Phil", age: 20}
get_in(user, [:name]) # => "Phil"Here are some Plugin Guides for creating your own plugins. Please check them out and feel free to copy-paste the code.