EctoTypedSchema (EctoTypedSchema v0.1.0)

Copy Markdown View Source

Getting Started

Use typed_schema as a drop-in replacement for Ecto.Schema.schema:

defmodule MyApp.Blog.Post do
  use EctoTypedSchema

  typed_schema "posts" do
    field :title, :string, typed: [null: false]
    field :status, Ecto.Enum, values: [:draft, :published]

    belongs_to :author, MyApp.Accounts.User
    has_many :comments, MyApp.Blog.Comment
    timestamps()
  end
end

This generates:

@type t() :: %MyApp.Blog.Post{
  __meta__: Ecto.Schema.Metadata.t(MyApp.Blog.Post),
  id: integer(),
  title: String.t(),
  status: :draft | :published | nil,
  author_id: integer() | nil,
  author: Ecto.Schema.belongs_to(MyApp.Accounts.User.t()) | nil,
  comments: Ecto.Schema.has_many(MyApp.Blog.Comment.t()),
  inserted_at: NaiveDateTime.t() | nil,
  updated_at: NaiveDateTime.t() | nil
}

You can verify the generated type in IEx:

iex> t MyApp.Blog.Post
@type t() :: %MyApp.Blog.Post{...}

Options

Type Parameters

Create parameterized types with parameter/2:

typed_embedded_schema type_kind: :opaque, type_name: :result, null: false do
  parameter :ok
  parameter :error

  field :ok, :string, typed: [type: ok]
  field :error, :string, typed: [type: error]
end
# Generates: @opaque result(ok, error) :: %__MODULE__{...}

Plugins

Register TypedStructor plugins to extend the generated type definition:

typed_schema "users" do
  plugin MyPlugin, some_option: true
  field :name, :string
end

Plugins are forwarded into the generated typed_structor block and receive all three callbacks (init, before_definition, after_definition).

Embedded Schemas

typed_embedded_schema do
  field :display_name, :string
  field :bio, :string
end

Embedded schema types omit __meta__.

Edge Cases

Through associations

through: associations are included in the generated type. If the chain can't be resolved at compile time, the type falls back to term() / list(term()) with a warning. Provide an explicit type to suppress:

has_many :post_tags, through: [:posts, :tags], typed: [type: list(Tag.t())]

belongs_to with define_field: false

No typed metadata is generated for the FK field. Define it manually with field/3 if you need custom type settings.

default: nil

Does not make a field non-nullable; the type stays ... | nil.

Typed Options

Pass typed: [...] on any field or association to customize its generated type:

OptionEffect
type:Override the inferred type entirely
null:false removes | nil from the type
default:Type metadata default; non-nil defaults imply non-nullable (runtime defaults come from Ecto default:)
field :email, :string, typed: [null: false]
field :role, :string, typed: [type: :admin | :user]

For belongs_to, the :foreign_key sub-option controls the FK field's type:

belongs_to :org, Organization,
  foreign_key: :org_id,
  typed: [foreign_key: [type: Ecto.UUID.t()]]

Schema-level Options

typed_schema/3 and typed_embedded_schema/2 accept options that apply as defaults to every field (per-field typed: options override):

typed_schema "users", null: false do
  field :name, :string
  field :bio, :string, typed: [null: true]  # override: nullable
end
OptionEffect
null:Default nullability for all fields
type_kind::opaque, :typep, etc. (default :type)
type_name:Custom type name (default :t)

Summary

Schema

Defines a typed embedded Ecto schema that delegates to Ecto.Schema.embedded_schema/1.

Defines a typed embedded Ecto schema with schema-level options.

Defines a typed Ecto schema that delegates directly to Ecto.Schema.schema/2.

Defines a typed Ecto schema with schema-level options.

Fields and Associations

Equivalent to embeds_many/4 without a block. See embeds_many/4 for details and examples.

Defines a typed Ecto.Schema.embeds_many/3 embed. Always non-nullable (defaults to []); the :null option has no effect.

Equivalent to embeds_one/4 without a block. See embeds_one/4 for details and examples.

Defines a typed Ecto.Schema.embeds_one/3 embed. Nullable by default.

Defines a typed schema field that wraps Ecto.Schema.field/3.

Defines a typed Ecto.Schema.has_many/3 association. Always non-nullable (defaults to []); the :null option has no effect.

Defines a typed Ecto.Schema.has_one/3 association. Nullable by default.

Defines a typed Ecto.Schema.many_to_many/3 association. Always non-nullable (defaults to []); the :null option has no effect.

Defines typed timestamp fields that wrap Ecto.Schema.timestamps/1.

Type Customization

Declares a type parameter for the schema's generated type.

Registers a TypedStructor plugin for the schema's generated type.

Schema

typed_embedded_schema(list)

(macro)
@spec typed_embedded_schema(keyword()) :: Macro.t()

Defines a typed embedded Ecto schema that delegates to Ecto.Schema.embedded_schema/1.

Equivalent to typed_embedded_schema([], do: block).

See typed_embedded_schema/2 for options and examples.

typed_embedded_schema(opts, list)

(macro)
@spec typed_embedded_schema(keyword(), keyword()) :: Macro.t()

Defines a typed embedded Ecto schema with schema-level options.

Wraps Ecto.Schema.embedded_schema/1 and captures type metadata for @type t() generation. Embedded schemas do not include a __meta__ field in their generated type.

Schema-level Options

Options that apply as defaults to every field (individual fields can override via their :typed option):

  • :null - if false, makes all field types non-nullable
  • :type_kind - the kind of type to generate (e.g., :opaque)
  • :type_name - custom name for the generated type (default: :t)

See the "Schema-level Options" section in the module documentation for more details.

Examples

typed_embedded_schema null: false do
  field :theme, :string
  field :bio, :string, typed: [null: true]  # override: nullable
end

typed_schema(source, list)

(macro)
@spec typed_schema(
  binary(),
  keyword()
) :: Macro.t()

Defines a typed Ecto schema that delegates directly to Ecto.Schema.schema/2.

Equivalent to typed_schema(source, [], do: block).

See typed_schema/3 for options and examples.

typed_schema(source, opts, list)

(macro)
@spec typed_schema(binary(), keyword(), keyword()) :: Macro.t()

Defines a typed Ecto schema with schema-level options.

Wraps Ecto.Schema.schema/2 and captures type metadata for @type t() generation via TypedStructor.

Schema-level Options

Options that apply as defaults to every field (individual fields can override via their :typed option):

  • :null - if false, makes all field types non-nullable
  • :type_kind - the kind of type to generate (e.g., :opaque)
  • :type_name - custom name for the generated type (default: :t)

See the "Schema-level Options" section in the module documentation for more details.

Examples

typed_schema "users", null: false do
  field :name, :string
  field :bio, :string, typed: [null: true]  # override: nullable
end

Fields and Associations

belongs_to(name, schema, opts \\ [])

(macro)

Defines a typed Ecto.Schema.belongs_to/3 association.

Creates both the association field and its foreign key field. The association is nullable by default.

Typed Options

  • :foreign_key - a keyword list to customize the foreign key field's type independently

See the "Typed options" section in the module documentation for more options.

Examples

belongs_to :user, User, typed: [null: false]
belongs_to :organization, Organization,
  foreign_key: :org_id,
  typed: [foreign_key: [type: Ecto.UUID.t()]]

embeds_many(name, schema, opts \\ [])

(macro)

Equivalent to embeds_many/4 without a block. See embeds_many/4 for details and examples.

embeds_many(name, schema, opts, list)

(macro)

Defines a typed Ecto.Schema.embeds_many/3 embed. Always non-nullable (defaults to []); the :null option has no effect.

The optional do block allows defining the embedded schema inline.

See the "Typed options" section in the module documentation for more options.

Examples

embeds_many :addresses, Address

embeds_many :line_items, LineItem, primary_key: false do
  field :product_name, :string
  field :quantity, :integer
  field :price, :decimal
end

embeds_one(name, schema, opts \\ [])

(macro)

Equivalent to embeds_one/4 without a block. See embeds_one/4 for details and examples.

embeds_one(name, schema, opts, list)

(macro)

Defines a typed Ecto.Schema.embeds_one/3 embed. Nullable by default.

The optional do block allows defining the embedded schema inline.

See the "Typed options" section in the module documentation for more options.

Examples

embeds_one :address, Address
embeds_one :profile, Profile, typed: [null: false]

embeds_one :address, Address, primary_key: false do
  field :street, :string
  field :city, :string
end

field(name, type \\ :string, opts \\ [])

(macro)

Defines a typed schema field that wraps Ecto.Schema.field/3.

Type Mapping

The generated type is inferred from the Ecto type. Common mappings include:

Ecto TypeElixir Typespec
:stringString.t()
:integerinteger()
:floatfloat()
:booleanboolean()
:binarybinary()
:decimalDecimal.t()
:dateDate.t()
:time / :time_usecTime.t()
:naive_datetime / :naive_datetime_usecNaiveDateTime.t()
:utc_datetime / :utc_datetime_usecDateTime.t()
:binary_idEcto.UUID.t()
:mapmap()
{:array, inner}list(inner_type)

| Ecto.Enum | Union of atom values (e.g., :active | :inactive) | | Custom module | Module.t() |

Ecto.Enum values are automatically captured and used to generate a union type.

See the "Typed options" section in the module documentation for more options.

Examples

field :name, :string
field :email, :string, typed: [null: false]
field :role, Ecto.Enum, values: [:admin, :user, :guest]

has_many(name, schema, opts \\ [])

(macro)

Defines a typed Ecto.Schema.has_many/3 association. Always non-nullable (defaults to []); the :null option has no effect.

Also supports through-associations via has_many :name, through: [:assoc, :chain].

See the "Typed options" section in the module documentation for more options.

Examples

has_many :posts, Post, foreign_key: :user_id
has_many :post_tags, through: [:posts, :tags], typed: [type: list(Tag.t())]

has_one(name, schema, opts \\ [])

(macro)

Defines a typed Ecto.Schema.has_one/3 association. Nullable by default.

See the "Typed options" section in the module documentation for more options.

Examples

has_one :profile, Profile
has_one :settings, Settings, typed: [null: false]

many_to_many(name, schema, opts \\ [])

(macro)

Defines a typed Ecto.Schema.many_to_many/3 association. Always non-nullable (defaults to []); the :null option has no effect.

See the "Typed options" section in the module documentation for more options.

Examples

many_to_many :tags, Tag, join_through: "posts_tags", typed: [type: list(Tag.t())]

timestamps(opts \\ [])

(macro)

Defines typed timestamp fields that wrap Ecto.Schema.timestamps/1.

The single :typed option applies to both generated timestamp fields.

Timestamp Type Mapping

Ecto TypeElixir Typespec
:naive_datetime (default)NaiveDateTime.t()
:naive_datetime_usecNaiveDateTime.t()
:utc_datetimeDateTime.t()
:utc_datetime_usecDateTime.t()

See the "Typed options" section in the module documentation for more options.

Examples

timestamps()
timestamps(type: :utc_datetime)
timestamps(typed: [null: false])

Type Customization

parameter(name, opts \\ [])

(macro)

Declares a type parameter for the schema's generated type.

Parameters make the generated type parameterized, e.g., @type t(age) :: %__MODULE__{...}. They are passed through to TypedStructor.parameter/2.

Examples

typed_schema "users" do
  parameter :age

  field :name, :string
  field :age, :integer, typed: [type: age]
end

This generates @type t(age) :: %__MODULE__{...} where the :age field uses the type parameter instead of the inferred integer() type.

plugin(plugin, opts \\ [])

(macro)

Registers a TypedStructor plugin for the schema's generated type.

Plugins are forwarded to the typed_structor block generated at compile time. They receive the TypedStructor.Definition and can modify types, add fields, or inject code before/after the struct definition.

See TypedStructor.Plugin for the plugin behaviour and callbacks.

Examples

typed_schema "users" do
  plugin MyPlugin, some_option: true

  field :name, :string
end