Skip to content

elixir-typed-structor/ecto_typed_schema

Repository files navigation

EctoTypedSchema

Hex.pm Hex Docs CI License: MIT

Ecto schemas don't generate @type t() specs. You either maintain them by hand (tedious, drifts out of sync) or skip them entirely (no Dialyzer/IDE support).

EctoTypedSchema infers types automatically from your Ecto field definitions -- just replace use Ecto.Schema with use EctoTypedSchema and schema with typed_schema.

Before -- manual @type that drifts out of sync:

defmodule MyApp.User do
  use Ecto.Schema

  schema "users" do
    field :name, :string
    field :age, :integer
    has_many :posts, MyApp.Post
  end

  # Must maintain by hand, easy to forget
  @type t() :: %__MODULE__{
    __meta__: Ecto.Schema.Metadata.t(),
    id: integer() | nil,
    name: String.t() | nil,
    age: integer() | nil,
    posts: list(MyApp.Post.t())
  }
end

After -- types inferred automatically:

defmodule MyApp.User do
  use EctoTypedSchema

  typed_schema "users" do
    field :name, :string
    field :age, :integer, typed: [null: false]
    has_many :posts, MyApp.Post
  end
end

Feature Highlights

  • Zero-annotation inference -- Ecto types mapped to typespecs automatically (:string -> String.t(), :integer -> integer(), etc.)
  • Association-aware -- belongs_to, has_many, has_one, many_to_many, and embeds all generate correct types
  • Ecto runtime semantics -- primary keys non-nullable, has_many/embeds_many default to [], and most other fields nullable
  • Fine-grained control -- override per-field with typed: [null: false] or typed: [type: ...]
  • Schema-level defaults -- set null:, type_kind:, type_name: for all fields at once
  • Through associations -- resolved at compile time with fallback warning
  • Plugin system -- forward TypedStructor plugins into the generated type block
  • Embedded schemas -- typed_embedded_schema works the same way, without __meta__

Installation

def deps do
  [
    {:ecto_typed_schema, "~> 0.1"},
    {:ecto, "~> 3.10"}
  ]
end

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.

Related

  • TypedStructor -- the type generation engine behind EctoTypedSchema
  • Ecto -- the database wrapper and query generator for Elixir

About

Auto-generate accurate @type t() specs from your Ecto schema definitions.

Resources

License

Stars

Watchers

Forks

Contributors

Languages