Defining schemas
Schema
defmodule Person do
use TypedEctoSchema
typed_schema "people" do
field(:name, :string, enforce: true, null: false)
field(:age, :integer) :: non_neg_integer() | nil
belongs_to(:company, Company)
timestamps()
end
endGenerates the schema, @enforce_keys [:name] and @type t().
Embedded schema
defmodule Address do
use TypedEctoSchema
typed_embedded_schema do
field(:street, :string)
field(:city, :string, null: false)
end
endtyped_embedded_schema replaces embedded_schema; everything else works the same.
Type inference
Fields
field(:name, :string)
# String.t() | nil
field(:count, :integer)
# integer() | nil
field(:tags, {:array, :string})
# list(String.t()) | nil
field(:role, Ecto.Enum, values: [:admin, :user])
# (:admin | :user) | nil
field(:price, Money)
# Money.t() | nil — custom types assume a t/0Associations and embeds
has_one(:profile, Profile)
# Ecto.Schema.has_one(Profile.t()) | nil
has_many(:posts, Post)
# Ecto.Schema.has_many(Post.t())
belongs_to(:company, Company)
# company: Ecto.Schema.belongs_to(Company.t()) | nil
# company_id: integer() | nil
embeds_one(:address, Address)
# Address.t() | nil
embeds_many(:entries, Entry)
# list(Entry.t())"Many" fields are always lists — never | nil.
Field options
:null — control | nil
field(:name, :string, null: false)
# name: String.t()
field(:phone, :string)
# phone: String.t() | nil (default)Never applies to has_many/embeds_many (always lists); has_one/belongs_to associations always get | nil.
:enforce — add to @enforce_keys
field(:name, :string, enforce: true)
# @enforce_keys [:name]:doc — document the field
field(:name, :string, doc: "The person's full name")Rendered through the moduledoc marker and the generated @typedoc (see below).
:: — override the typespec
field(:age, :integer) :: non_neg_integer() | nil
# age: non_neg_integer() | nil (verbatim)The override always wins over inference and is used exactly as written — add | nil yourself if wanted.
Schema-level options
Defaults for all fields
typed_schema "people", null: false, enforce: true do
# String.t(), enforced
field(:name, :string)
# opt back out per field
field(:age, :integer, null: true)
# defaults are not enforced
field(:bio, :string, default: "")
endOther options
typed_schema "people", opaque: true do
# @opaque t()
end
typed_schema "people", additional_types: true do
# named types for Ecto.Enum fields (experimental)
endGenerated fields
@primary_key
@primary_key {:id, :binary_id,
autogenerate: true, null: false}
# id: binary() (no | nil):null and :enforce work here too — they are stripped before Ecto sees them.
timestamps/1
timestamps() # NaiveDateTime.t() | nil
timestamps(null: false) # NaiveDateTime.t()
timestamps(type: :utc_datetime) # DateTime.t() | nilNullable by default: a struct that was not inserted yet has nil timestamps.
belongs_to/3 foreign key
belongs_to(:company, Company, null: false)
# company: Ecto.Schema.belongs_to(Company.t()) | nil
# company_id: integer():null applies to the generated foreign key; :enforce enforces the association field.
Documenting fields
The moduledoc marker
defmodule Person do
@moduledoc """
A person.
## Fields
<!-- typed_ecto_schema: fields -->
"""
use TypedEctoSchema
typed_schema "people" do
field(:name, :string, doc: "The person's full name")
field(:age, :integer)
end
endThe marker is replaced at compile time with a list of every field, its typespec and its :doc:
- `id` (`integer() | nil`)
- `name`: The person's full name (`String.t()`)
- `age` (`integer() | nil`)The marker is the only trigger — without it, :doc options don't touch the @moduledoc.
Controlling the generated @typedoc
# Default: t/0 gets a @typedoc with the
# fields list, for every schema
typed_schema "people" do
...
end# Your own @typedoc is kept; the marker
# is interpolated in it too
@typedoc """
A person. Prefer building via new/1.
<!-- typed_ecto_schema: fields -->
"""
typed_schema "people" do
...
end# Suppress it entirely
@typedoc false
typed_schema "people" do
...
endField docs show up in t Person.t() in IEx and on hover in editors.
Experimental features
Named types for Ecto.Enum
typed_schema "people", additional_types: true do
field(:role, Ecto.Enum, values: [:admin, :user])
end
# @type role() :: :admin | :user
# usable as Person.role()Or enable it globally (compile-time config; the schema-level option still wins in both directions):
# config/config.exs
config :typed_ecto_schema, additional_types: trueFor {:array, Ecto.Enum} the named type is the element union — write list(Person.roles()) for the list.
PolymorphicEmbed integration
# config/config.exs (compile-time, off by default)
config :typed_ecto_schema, polymorphic_embed: truepolymorphic_embeds_one(:channel,
types: [sms: SMS, email: Email],
on_replace: :update
)
# channel: (SMS.t() | Email.t()) | nil
polymorphic_embeds_many(:channels,
types: [sms: SMS, email: Email],
on_replace: :delete
)
# channels: list(SMS.t() | Email.t())::, :null, :enforce and :doc work as on field/3. With additional_types: true these fields get named types too (element union for _many). polymorphic_embed stays out of this library's deps — add it to yours.