Skip to content

feat: opt-in named types for Ecto.Enum fields (#39) - #63

Merged
bamorim merged 4 commits into
masterfrom
feat-39-enum-types
Aug 25, 2026
Merged

bamorim merged 4 commits into
masterfrom
feat-39-enum-types

Conversation

@bamorim

@bamorim bamorim commented Aug 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds an opt-in, experimental additional_types schema-level option to typed_schema and typed_embedded_schema (alongside the existing :null, :enforce and :opaque options). When enabled, each Ecto.Enum field defines a public named type with the union of its values, so it can be referenced from other modules' specs:

defmodule Person do
  use TypedEctoSchema

  typed_schema "people", additional_types: true do
    field(:role, Ecto.Enum, values: [:admin, :user])
  end
end

# Defines @type role() :: :admin | :user, usable as Person.role()

Two follow-up additions after rebasing onto 0.4.4 + #64:

  • Global opt-in via compile-time config (in addition to the per-schema option): config :typed_ecto_schema, additional_types: true, read with Application.compile_env/4 like feat: support polymorphic_embed's polymorphic_embeds_one/many (#40) #64's polymorphic_embed flag, so schemas recompile when it changes. The schema-level option wins in both directions (additional_types: false opts a schema out of the global default).
  • Named types for polymorphic embeds: when both the PolymorphicEmbed integration (feat: support polymorphic_embed's polymorphic_embeds_one/many (#40) #64) and additional_types are enabled, polymorphic_embeds_one(:channel, types: [sms: SMS, email: Email]) also defines @type channel() :: SMS.t() | Email.t() (element union for _many, mirroring the {:array, Ecto.Enum} behavior). This reuses the module-union AST the syntax sugar already builds for the inline typespec, so module aliases are still never resolved; the any() fallback for unresolvable types: emits no named type.

Behavior

  • Experimental — documented as such in the README and moduledoc; behavior may change in future releases.
  • Off by default — zero change to existing schemas, and no application/compile config involved.
  • Keyword values (values: [foo: 1, bar: 2]) generate the union of the atom keys (:foo | :bar).
  • {:array, Ecto.Enum} fields generate the union of the element values, since that's what's useful in other specs.
  • Values referenced through module attributes (values: @role_values) resolve fine, since options are evaluated in the module body before the type builder runs — so the exact example from the issue gets a named type too.
  • Silently skipped: non-enum fields, fields whose :values can't be resolved to a list of atoms, and fields named t (which would collide with the schema's own t/0). Other name collisions (e.g. a field named after a user-defined or built-in type) error naturally at compile time; this is documented in the moduledoc and README.
  • No changes to EctoTypeMapper: an earlier revision added a fallback clause for unresolvable :values, but after rebasing onto master that is already covered by enum_type/1 from fix: handle empty and non-literal Ecto.Enum values (#57) #62, so it was dropped.

Tests

Additions: global config on (named types emitted with no schema option), schema-level additional_types: false overriding the global config, polymorphic_embeds_one/_many module unions, unresolvable polymorphic types: emitting nothing.

  • enabled + enum field → named public type exported, matching the values union
  • enabled + keyed values → union of keys
  • enabled + {:array, Ecto.Enum} → element union
  • enabled + values from a module attribute → resolved union
  • enabled + unresolvable values → silently skipped, no crash
  • enabled + field named t / non-enum fields → skipped
  • typed_embedded_schema support
  • disabled (default) → only t/0 exported

mix test && mix credo --strict && mix dialyzer all pass.

Closes #39

🤖 Generated with Claude Code

@bamorim
bamorim force-pushed the feat-39-enum-types branch from 6fda359 to 90f9700 Compare August 24, 2026 09:54
bamorim and others added 3 commits August 25, 2026 16:28
Add an opt-in `additional_types` schema-level option to typed_schema
and typed_embedded_schema. When enabled, each Ecto.Enum field defines a
public named type with the union of its values (union of atom keys for
keyword values, element union for {:array, Ecto.Enum}), so it can be
referenced from other modules' specs as MySchema.field_name().

Fields named t and fields whose values can't be resolved to a list of
atoms are silently skipped. Also add a fallback clause to
disjunction_typespec/1 so unresolvable values fall back to any()
instead of raising.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Allow enabling additional_types globally via compile-time application
config (config :typed_ecto_schema, additional_types: true), with the
schema-level option taking precedence in both directions, following the
same Application.compile_env pattern as the polymorphic_embed flag.

When both the PolymorphicEmbed integration and additional_types are
enabled, polymorphic_embeds_one/many fields also emit a named type with
the union of the modules in their :types option, reusing the union AST
the syntax sugar already builds (skipping the any() fallback for
unresolvable types and fields named t).

Rename the internal enum_types accumulator to additional_types since it
is no longer enum-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bamorim
bamorim force-pushed the feat-39-enum-types branch from 90f9700 to da4d0a1 Compare August 25, 2026 15:32
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bamorim
bamorim merged commit 35be60c into master Aug 25, 2026
15 checks passed
@bamorim bamorim mentioned this pull request Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support accessing individual field's type

1 participant