TypedStruct v0.1.0 TypedStruct View Source

TypedStruct is a library for defining structs with a type without writing boilerplate code.

Rationale

To define a struct in Elixir, you probably want to define three things:

  • the struct itself, with default values,
  • the list of enforced keys,
  • its associated type.

It ends up in something like this:

defmodule Person do
  @moduledoc """
  A struct representing a person.
  """

  @enforce_keys [:name]
  defstruct name: nil,
            age: nil,
            happy?: true,
            phone: nil

  @typedoc "A person"
  @type t() :: %__MODULE__{
          name: String.t(),
          age: non_neg_integer() | nil,
          happy?: boolean() | nil,
          phone: String.t() | nil
        }
end

In the example above you can notice several points:

  • the keys are present in both the defstruct and type definition,
  • enforced keys must also be written in @enforce_keys,
  • if a key is not enforced, the type should be nullable.

If you want to add a field in the struct, you must therefore:

  • add the key with its default value in the defstruct list,
  • add the key with its type in the type definition.

If the field is not optional, you should even add it to @enforce_keys. This is way too much work for lazy people like me, and moreover it can be error-prone.

It would be way better if we could write something like this:

defmodule Person do
  @moduledoc """
  A struct representing a person.
  """

  use TypedStruct

  @typedoc "A person"
  typedstruct do
    field :name, String.t(), enforce: true
    field :age, non_neg_integer()
    field :happy?, boolean(), default: true
    field :phone, String.t()
  end
end

Thanks to TypedStruct, this is now possible :)

Usage

Setup

To use TypedStruct in your project, add this to you Mix dependencies:

{:typed_struct, "~> 0.1.0", runtime: false}

If you want to avoid mix format putting parentheses on field definitions, you can write in your .formatter.exs:

[
  import_deps: [:typed_struct]
]

General usage

To define a typed struct, use TypedStruct, then define your struct within a typedstruct block:

defmodule MyStruct do
  # Use TypedStruct to import the typedstruct macro
  use TypedStruct

  # Define your struct
  typedstruct do
    # Define each field with the field macro
    field :a_string, String.t()

    # You can set a default value
    field :string_with_default, String.t(), default: "default"

    # You can enforce a field
    field :enforced_field, integer(), enforce: true
  end
end

Each field is defined through the field/2 macro.

Documentation

To add a @typedoc to the struct type, just add the attribute above the typedstruct block:

@typedoc "A typed struct"
typedstruct do
  field :a_string, String.t()
  field :an_int, integer()
end

Reflexion

To enable the use of information defined by TypedStruct by other modules, each typed struct defines three functions:

  • __keys__/0 - returns the keys of the struct
  • __defaults__/0 - returns the default value for each field
  • __types__/0 - returns the quoted type for each field

For instance:

iex(1)> defmodule Demo do
...(1)>   use TypedStruct
...(1)>
...(1)>   typedstruct do
...(1)>     field :a_field, String.t()
...(1)>     field :with_default, integer(), default: 7
...(1)>   end
...(1)> end
{:module, Demo,
<<70, 79, 82, 49, 0, 0, 8, 60, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 241,
  0, 0, 0, 24, 11, 69, 108, 105, 120, 105, 114, 46, 68, 101, 109, 111, 8, 95,
  95, 105, 110, 102, 111, 95, 95, 9, 102, ...>>, {:__types__, 0}}
iex(2)> Demo.__keys__()
[:a_field, :with_default]
iex(3)> Demo.__defaults__()
[a_field: nil, with_default: 7]
iex(4)> Demo.__types__()
[
  a_field: {:|, [],
  [
    {{:., [line: 5],
      [{:__aliases__, [line: 5, counter: -576460752303422524], [:String]}, :t]},
      [line: 5], []},
    nil
  ]},
  with_default: {:|, [], [{:integer, [line: 6], []}, nil]}
]

What do I get?

When defining an empty typedstruct block:

defmodule Example do
  use TypedStruct

  typedstruct do
  end
end

you get an empty struct with its module type t():

defmodule Example do
  @enforce_keys []
  defstruct []

  @type t() :: %__MODULE__{}
end

Each field call adds information to the struct, @enforce_keys and the type t().

A field with no options adds the name to the defstruct list, with nil as default. The type itself is made nullable:

defmodule Example do
  use TypedStruct

  typedstruct do
    field :name, String.t()
  end
end

becomes:

defmodule Example do
  @enforce_keys []
  defstruct name: nil

  @type t() :: %__MODULE__{
          name: String.t() | nil
        }
end

The default option adds the default value to the defstruct:

field :name, String.t(), default: "John Smith"

# Becomes
defstruct name: "John Smith"

The type itself remains the same.

The enforce option, when set to true, enforces the key. Therefore, the type is not nullable anymore:

defmodule Example do
  use TypedStruct

  typedstruct do
    field :name, String.t(), enforce: true
  end
end

becomes:

defmodule Example do
  @enforce_keys [:name] # :name is enforced
  defstruct name: nil

  @type t() :: %__MODULE__{
          name: String.t() # Not nullable
        }
end

Link to this section Summary

Functions

Defines a field in a typed struct

Defines a typed struct

Link to this section Functions

Link to this macro field(name, type, opts \\ []) View Source (macro)

Defines a field in a typed struct.

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
Link to this macro typedstruct(list) View Source (macro)

Defines a typed struct.

Inside a typedstruct block, each field is defined through the field/2 macro.