# `EctoTypedSchema`
[🔗](https://github.com/elixir-typed-structor/ecto_typed_schema/blob/v0.1.0/lib/ecto_typed_schema.ex#L1)

## Getting Started

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

```elixir
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:

```elixir
@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`:

```elixir
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](https://hexdocs.pm/typed_structor/TypedStructor.Plugin.html)
to extend the generated type definition:

```elixir
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

```elixir
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:

```elixir
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:

| Option | Effect |
| --- | --- |
| `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:`) |

```elixir
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:

```elixir
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):

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

| Option | Effect |
| --- | --- |
| `null:` | Default nullability for all fields |
| `type_kind:` | `:opaque`, `:typep`, etc. (default `:type`) |
| `type_name:` | Custom type name (default `:t`) |

# `typed_embedded_schema`
*macro* 

```elixir
@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`
*macro* 

```elixir
@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"](`m:EctoTypedSchema#module-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`
*macro* 

```elixir
@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`
*macro* 

```elixir
@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"](`m:EctoTypedSchema#module-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

# `belongs_to`
*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"](`m:EctoTypedSchema#module-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`
*macro* 

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

# `embeds_many`
*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"](`m:EctoTypedSchema#module-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`
*macro* 

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

# `embeds_one`
*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"](`m:EctoTypedSchema#module-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`
*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 Type | Elixir Typespec |
|---|---|
| `:string` | `String.t()` |
| `:integer` | `integer()` |
| `:float` | `float()` |
| `:boolean` | `boolean()` |
| `:binary` | `binary()` |
| `:decimal` | `Decimal.t()` |
| `:date` | `Date.t()` |
| `:time` / `:time_usec` | `Time.t()` |
| `:naive_datetime` / `:naive_datetime_usec` | `NaiveDateTime.t()` |
| `:utc_datetime` / `:utc_datetime_usec` | `DateTime.t()` |
| `:binary_id` | `Ecto.UUID.t()` |
| `:map` | `map()` |
| `{: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"](`m:EctoTypedSchema#module-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`
*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"](`m:EctoTypedSchema#module-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`
*macro* 

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

See the ["Typed options"](`m:EctoTypedSchema#module-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`
*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"](`m:EctoTypedSchema#module-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`
*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 Type | Elixir Typespec |
|---|---|
| `:naive_datetime` (default) | `NaiveDateTime.t()` |
| `:naive_datetime_usec` | `NaiveDateTime.t()` |
| `:utc_datetime` | `DateTime.t()` |
| `:utc_datetime_usec` | `DateTime.t()` |

See the ["Typed options"](`m:EctoTypedSchema#module-typed-options`) section in the
module documentation for more options.

## Examples

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

# `parameter`
*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`
*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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
