TypedStruct is a library for defining structs with a type without writing boilerplate code.
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:
defmodulePersondo@moduledoc""" A struct representing a person. """@enforce_keys[:name]defstructname: nil,age: nil,happy?: true,phone: nil@typedoc"A person"@typet()::%__MODULE__{name: String.t(),age: non_neg_integer()|nil,happy?: boolean(),phone: String.t()|nil}endIn the example above you can notice several points:
- the keys are present in both the
defstructand type definition, - enforced keys must also be written in
@enforce_keys, - if a key has no default value and is not enforced, its 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
defstructlist, - 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:
defmodulePersondo@moduledoc""" A struct representing a person. """useTypedStructtypedstructdo@typedoc"A person"field:name,String.t(),enforce: truefield:age,non_neg_integer()field:happy?,boolean(),default: truefield:phone,String.t()endendThanks to TypedStruct, this is now possible :)
To use TypedStruct in your project, add this to your Mix dependencies:
{:typed_struct,"~> 0.3.0"}If you do not plan to compile modules using TypedStruct at runtime, you can add
runtime: false to the dependency tuple as TypedStruct is only used at build
time.
If you want to avoid mix format putting parentheses on field definitions,
you can add to your .formatter.exs:
[...,import_deps: [:typed_struct]]To define a typed struct, use
TypedStruct, then define
your struct within a typedstruct block:
defmoduleMyStructdo# Use TypedStruct to import the typedstruct macro.useTypedStruct# Define your struct.typedstructdo# 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: trueendendEach field is defined through the
field/2 macro.
If you want to enforce all the keys by default, you can do:
defmoduleMyStructdouseTypedStruct# Enforce keys by default.typedstructenforce: truedo# This key is enforced.field:enforced_by_default,term()# You can override the default behaviour.field:not_enforced,term(),enforce: false# A key with a default value is not enforced.field:not_enforced_either,integer(),default: 1endendYou can also generate an opaque type for the struct:
defmoduleMyOpaqueStructdouseTypedStruct# Generate an opaque type for the struct.typedstructopaque: truedofield:name,String.t()endendIf you often define submodules containing only a struct, you can avoid boilerplate code:
defmoduleMyModuledouseTypedStruct# You now have %MyModule.Struct{}.typedstructmodule: Structdofield:field,term()endendTo add a @typedoc to the struct type, just add the attribute in the
typedstruct block:
typedstructdo@typedoc"A typed struct"field:a_string,String.t()field:an_int,integer()endYou can also document submodules this way:
typedstructmodule: MyStructdo@moduledoc"A submodule with a typed struct."@typedoc"A typed struct in a submodule"field:a_string,String.t()field:an_int,integer()endIt is possible to extend the scope of TypedStruct by using its plugin interface,
as described in
TypedStruct.Plugin.
For instance, to automatically generate lenses with the
Lens library, you can use
TypedStructLens and do:
defmoduleMyStructdouseTypedStructtypedstructdopluginTypedStructLensfield:a_field,String.t()field:other_field,atom()end@specchange(t())::t()defchange(data)do# a_field/0 is generated by TypedStructLens.lens=a_field()put_in(data,[lens],"Changed")endendtyped_struct_lens– Integration with the Lens library.typed_struct_legacy_reflection– Re-enables the legacy reflection functions from TypedStruct 0.1.x.
This list is not meant to be exhaustive, please search for “typed_struct” on hex.pm for other results. If you want your plugin to appear here, please open an issue.
When defining an empty typedstruct block:
defmoduleExampledouseTypedStructtypedstructdoendendyou get an empty struct with its module type t():
defmoduleExampledo@enforce_keys[]defstruct[]@typet()::%__MODULE__{}endEach 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:
defmoduleExampledouseTypedStructtypedstructdofield:name,String.t()endendbecomes:
defmoduleExampledo@enforce_keys[]defstructname: nil@typet()::%__MODULE__{name: String.t()|nil}endThe default option adds the default value to the defstruct:
field:name,String.t(),default: "John Smith"# Becomesdefstructname: "John Smith"When set to true, the enforce option enforces the key by adding it to the
@enforce_keys attribute.
field:name,String.t(),enforce: true# Becomes@enforce_keys[:name]defstructname: nilIn both cases, the type has no reason to be nullable anymore by default. In one
case the field is filled with its default value and not nil, and in the other
case it is enforced. Both options would generate the following type:
@typet()::%__MODULE__{name: String.t()# Not nullable}Passing opaque: true replaces @type with @opaque in the struct type
specification:
typedstructopaque: truedofield:name,String.t()endgenerates the following type:
@opaquet()::%__MODULE__{name: String.t()}When passing module: ModuleName, the whole typedstruct block is wrapped in a
module definition. This way, the following definition:
defmoduleMyModuledouseTypedStructtypedstructmodule: Structdofield:field,term()endendbecomes:
defmoduleMyModuledodefmoduleStructdo@enforce_keys[]defstructfield: nil@typet()::%__MODULE__{field: term()|nil}endend- Struct definition
- Type definition (with nullable types)
- Default values
- Enforced keys (non-nullable types)
- Plugin API
- Default value type-checking (is it possible?)
- Guard generation
- Integration with Lens
- Integration with Ecto
- Domo: a library to validate structs that
define a
t()type, like the one generated byTypedStruct. - TypedEctoSchema: a library
that provides a DSL on top of
Ecto.Schemato achieve the same result asTypedStruct, withEcto.
Before contributing to this project, please read the CONTRIBUTING.md.
Copyright © 2018-2022 Jean-Philippe Cugnet and Contributors
This project is licensed under the MIT license.