View Source TypedStructor (TypedStructor v0.5.0)

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

Installation

Add :typed_structor to the list of dependencies in mix.exs:

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

Add :typed_structor to your .formatter.exs file

[
  # import the formatter rules from `:typed_structor`
  import_deps: [..., :typed_structor],
  inputs: [...]
]

Usage

General usage

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
end

This 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()
  }
end

Check TypedStructor.typed_structor/2 and TypedStructor.field/3 for more information.

Options

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
end

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

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

becomes:

defmodule User do
  @type t(id, name) :: %__MODULE__{
    id: id | nil,
    name: name | nil,
    age: non_neg_integer() | nil
  }

  defstruct [:id, :name, :age]
end

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

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

Furthermore, 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
end

Now, 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" => "my@email.com"})
#Ecto.Changeset<
  action: nil,
  changes: %{email: "my@email.com"},
  errors: [],
  data: #User.Profile<>,
  valid?: true
>

Define an Exception

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
end

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

Documentation

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

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

Plugins

TypedStructor 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"

Plugins guides

Here are some Plugin Guides for creating your own plugins. Please check them out and feel free to copy-paste the code.

Summary

Functions

Defines a field in a typed_structor/2. You can override the options set by typed_structor/2 by passing options.

Defines a type parameter in a typed_structor/2.

Registers a plugin for the currently defined struct.

Defines a struct with type information.

Functions

Link to this macro

field(name, type, options \\ [])

View Source (macro)

Defines a field in a typed_structor/2. You can override the options set by typed_structor/2 by passing options.

Example

# A field named :example of type String.t()
field :example, String.t()

Options

  • :default - sets the default value for the field
  • :enforce - if set to true, enforces the field, and makes its type non-nullable if :default is not set

How :default and :enforce affect type and @enforce_keys

:default:enforcetype@enforce_keys
settruet()excluded
setfalset()excluded
unsettruet()included
unsetfalset() | nilexcluded
Link to this macro

parameter(name, opts \\ [])

View Source (macro)

Defines a type parameter in a typed_structor/2.

Example

# A type parameter named int
parameter :int

fied :number, int # not int()
Link to this macro

plugin(plugin, opts \\ [])

View Source (macro)

Registers a plugin for the currently defined struct.

Example

typed_structor do
  plugin MyPlugin

  field :string, String.t()
end

For more information on how to define your own plugins, please see TypedStructor.Plugin. To use a third-party plugin, please refer directly to its documentation.

Link to this macro

typed_structor(options \\ [], list)

View Source (macro)

Defines a struct with type information.

Inside a typed_structor block, you can define fields with the field/3 macro.

Options

  • :module - if provided, a new submodule will be created with the struct.
  • :enforce - if true, the struct will enforce the keys, see field/3 options for more information.
  • :definer - the definer module to use to define the struct, record or exception. Defaults to :defstruct. It also accepts a macro that receives the definition struct and returns the AST. See definer section below.
  • :type_kind - the kind of type to use for the struct. Defaults to type, can be opaque or typep.
  • :type_name - the name of the type to use for the struct. Defaults to t.

Definer

The available definers are:

  • :defstruct, which defines a struct and a type for a given definition
  • :defexception, which defines an exception and a type for a given definition
  • :defrecord, which defines record macros and a type for a given definition
  • :defrecordp, which defines private record macros and a type for a given definition

:defstruct options

  • :define_struct - if false, the type will be defined, but the struct will not be defined. Defaults to true.

:defexception options

  • :define_struct - if false, the type will be defined, but the struct will not be defined. Defaults to true.

:defrecord and :defrecordp options

  • :record_name(required) - the name of the record, it must be provided.
  • :record_tag - if set, the record will be tagged with the given value. Defaults to nil.
  • :define_record - if false, the type will be defined, but the record will not be defined. Defaults to true.

custom definer

defmodule MyStruct do
  use TypedStructor

  typed_structor definer: MyDefiner do

    field :name, String.t()
    field :age, integer()
  end
end

Examples

defmodule MyStruct do
  use TypedStructor

  typed_structor do
    field :name, String.t()
    field :age, integer()
  end
end

Creates the struct in a submodule instead:

defmodule MyStruct do
  use TypedStructor

  typed_structor module: Struct do
    field :name, String.t()
    field :age, integer()
  end
end

To add a @typedoc to the struct type and @moduledoc to the submodule, just add the module attribute in the typed_structor block:

defmodule MyStruct do
  use TypedStructor

  typed_structor module: Struct do
    @typedoc "A typed struct"
    @moduledoc "A submodule"

    field :name, String.t()
    field :age, integer()
  end
end