TypedStructor eliminates the boilerplate of defining Elixir structs, type specs, and enforced keys separately. Define them once, keep them in sync automatically.
Before -- three declarations that must stay in sync manually:
defmodule User do
@enforce_keys [:id]
defstruct [:id, :name, :age]
@type t() :: %__MODULE__{
id: pos_integer(),
name: String.t() | nil,
age: non_neg_integer() | nil
}
endAfter -- a single source of truth:
defmodule User do
use TypedStructor
typed_structor do
field :id, pos_integer(), enforce: true
field :name, String.t()
field :age, non_neg_integer()
end
end- Single definition -- struct, type spec, and
@enforce_keysgenerated from one block - Nullable by default -- unenforced fields without defaults automatically include
| nil - Fine-grained null control -- override nullability per-field or per-block with the
:nulloption - Opaque and custom types -- generate
@opaque,@typep, or rename the type fromt() - Type parameters -- define generic/parametric types
- Multiple definers -- supports structs, exceptions, and Erlang records
- Plugin system -- extend behavior at compile time with composable plugins
- Nested modules -- define structs in submodules with the
:moduleoption
Add :typed_structor to your dependencies in mix.exs:
def deps do
[
{:typed_structor, "~> 0.6"}
]
endAdd
:typed_structorto your.formatter.exsfor proper indentation:[ import_deps: [..., :typed_structor], inputs: [...] ]
Use typed_structor blocks to define fields with their types:
defmodule User do
use TypedStructor
typed_structor do
field :id, pos_integer(), enforce: true # Required, never nil
field :name, String.t() # Optional, nullable
field :role, String.t(), default: "user" # Has default, not nullable
end
endThe interaction between :enforce, :default, and :null determines whether a field's type includes nil:
:default |
:enforce |
:null |
Type includes nil? |
|---|---|---|---|
unset |
false |
true |
yes |
unset |
false |
false |
no |
set |
- | - | no |
| - | true |
- | no |
You can set :null at the block level to change the default for all fields:
typed_structor null: false do
field :id, integer() # Not nullable
field :email, String.t() # Not nullable
field :phone, String.t(), null: true # Override: nullable
endUse type_kind: :opaque to hide implementation details:
typed_structor type_kind: :opaque do
field :secret, String.t()
end
# Generates: @opaque t() :: %__MODULE__{...}Override the default t() type name:
typed_structor type_name: :user_data do
field :id, pos_integer()
end
# Generates: @type user_data() :: %__MODULE__{...}Create generic types with parameter/1:
typed_structor do
parameter :value_type
parameter :error_type
field :value, value_type
field :error, error_type
end
# Generates: @type t(value_type, error_type) :: %__MODULE__{...}Define structs in submodules:
defmodule User do
use TypedStructor
typed_structor module: Profile do
field :email, String.t(), enforce: true
field :bio, String.t()
end
end
# Creates User.Profile with its own struct and typeExtend TypedStructor's behavior with plugins that run at compile time:
typed_structor do
plugin Guides.Plugins.Accessible
field :id, pos_integer()
field :name, String.t()
endSee the Plugin Guides for examples and instructions on writing your own.
Add @typedoc inside the block, and @moduledoc at the module level as usual:
defmodule User do
@moduledoc "User account data"
use TypedStructor
typed_structor do
@typedoc "A user with authentication details"
field :id, pos_integer()
field :name, String.t()
end
endDefine typed exceptions with automatic __exception__ handling:
defmodule HTTPException do
use TypedStructor
typed_structor definer: :defexception, enforce: true do
field :status, non_neg_integer()
field :message, String.t()
end
@impl Exception
def message(%__MODULE__{status: status, message: msg}) do
"HTTP #{status}: #{msg}"
end
endCreate Erlang-compatible records:
defmodule UserRecord do
use TypedStructor
typed_structor definer: :defrecord, record_name: :user do
field :name, String.t(), enforce: true
field :age, pos_integer(), enforce: true
end
endUse define_struct: false to skip struct generation when another library defines the struct:
defmodule User do
use TypedStructor
typed_structor define_struct: false do
field :email, String.t(), enforce: true
use Ecto.Schema
@primary_key false
schema "users" do
Ecto.Schema.field(:email, :string)
end
end
endThis generates only the type spec while letting the other library handle the struct definition.
For full Ecto integration with typed fields, see EctoTypedSchema -- a companion library built on TypedStructor.
- HexDocs -- full API reference and guides
- Plugin Guides -- build and use plugins
- Changelog -- release history