Skip to content

elixir-typed-structor/typed_structor

Repository files navigation

TypedStructor

Build Status Hex.pm HexDocs Plugin guides

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
  }
end

After -- 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

Feature Highlights

  • Single definition -- struct, type spec, and @enforce_keys generated 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 :null option
  • Opaque and custom types -- generate @opaque, @typep, or rename the type from t()
  • 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 :module option

Installation

Add :typed_structor to your dependencies in mix.exs:

def deps do
  [
    {:typed_structor, "~> 0.6"}
  ]
end

Formatter Setup {: .tip}

Add :typed_structor to your .formatter.exs for proper indentation:

[
  import_deps: [..., :typed_structor],
  inputs: [...]
]

Getting Started

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
end

Nullability Rules

The 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
end

Options

Opaque Types

Use type_kind: :opaque to hide implementation details:

typed_structor type_kind: :opaque do
  field :secret, String.t()
end
# Generates: @opaque t() :: %__MODULE__{...}

Custom Type Names

Override the default t() type name:

typed_structor type_name: :user_data do
  field :id, pos_integer()
end
# Generates: @type user_data() :: %__MODULE__{...}

Type Parameters

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__{...}

Nested Modules

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 type

Plugins

Extend 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()
end

See the Plugin Guides for examples and instructions on writing your own.

Documentation

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
end

Advanced Usage

Exceptions

Define 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
end

Records

Create 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
end

Integration with Other Libraries

Use 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
end

This 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.

Learn More

About

TypedStructor is a library for defining typed structs, exceptions and record macros with effortlessly.

Resources

License

Stars

Watchers

Forks

Contributors

Languages