Adding typespect to an Ecto Schema

I have an Ecto schema and I am wondering about how to represent it in typespecs.

Suppose that my schema is like this:

defmodule User do
  use MyApp.Model
  
  schema "users" do
    field :name, :string
    field :email, :string
    timestamps
  end
end

Which would be the best way to refer to this struct in my specs? Currently I am doing something like:

@spec my_function(String.t) :: %User{}

But I’m thinking that I could add a @type t :: %__MODULE__{user: String.t, email: String.t} declaration to my User struct and change the specs to something like:

@spec my_function(String.t) :: User.t

Which alternative would be the best? And why?

1 Like

I believe @type t ... is the convention to define the type of module and hence should be preferred - it’s the first thing I’d try in an application. Also if you define the required keys like you showed, then you don’t have to repeat that knowledge everywhere and the source of truth lives in Schema.t

2 Likes

I have this struct but I don’t know how to make the typespec for the associations

%Core.Models.Service{__meta__: #Ecto.Schema.Metadata<:built, "services">,
 charge_to: nil, customer_id: nil, estimated_price: nil,
 estimated_time: nil, id: nil, inserted_at: nil,
 organization_id: nil, payment_type: nil, price: nil,
 scheduled_time: nil,
 service_type: #Ecto.Association.NotLoaded<association :service_type is not loaded>,
 service_type_id: nil, status: nil,
 tasks: #Ecto.Association.NotLoaded<association :tasks is not loaded>,
 total_km: nil, total_time: nil, updated_at: nil,
 user_id: nil, uuid: nil,
 worker_vehicle: #Ecto.Association.NotLoaded<association :worker_vehicle is not loaded>,
 worker_vehicle_id: nil}
2 Likes

I’ve got the same problem. I tried multiple types but dialyzer show me warnings all times.

I’d say if Ecto does not already setup a typespec for your schema when it defines the struct, then I’d consider that a bug. This should happen automatically IMO.

5 Likes

Yes. I completely agree with you.
Typespecs should be provided automatically for Ecto schemas.

While it would be rather easy to generate a spec for a schema with built-in types, it’s not as easy to generate one when you start using custom types.

1 Like

Oh, I understand.
So, if the way to go is to manually add the typespecs to our schemas, do you think that it would be a good idea to mention it in the Ecto.Schema docs.

I could submit a PR myself to improve the docs.

Do you have an example about how are you doing it?

Would it be possible to return just term for custom types that does not provide the callback?

I found https://github.com/elixir-ecto/ecto/issues/425 and I agree with @whatyouhide

I think even just laying out the struct info is a good starting point. I also think that most people would implement the callback in Ecto.Type if we give good documentation on how to do it; it doesn’t sound like a very complicated task if you have a “template” to look at.

I might be wrong, but my first guess is that it would be something like quote(do: MyCustomType.t)

What do you think?

Could someone check if the LOC below are flawless, especially:

  • the virtual field
  • the associations

teacher_id: integer() or should it be teacher: Teacher.t()
user_id: integer() or should it be user: User.t()

# user.ex

@type t :: %__MODULE__{
  id: integer,
  email: String.t(),
  name:  String.t(),
  phone: String.t(),

  credential: Credential.t(),
  teacher_id: integer,
  student_id: integer,

  inserted_at: NaiveDateTime.t(),
  updated_at: NaiveDateTime.t()
  }

schema "users" do
  field(:email, :string)
  field(:name, :string)
  field(:phone, :string)

   has_one(:credential, Credential)
   belongs_to(:teacher, Teacher)
   belongs_to(:student, Student)

   timestamps()
end

# credential.ex

@type t :: %__MODULE__{
 password: String.t(),
 password_hash: String.t(),
 
  user_id: integer,

  inserted_at: NaiveDateTime.t(),
  updated_at: NaiveDateTime.t()
}

schema "credentials" do
  field(:password_hash, :string)
  # Virtual Fields
  field(:password, :string, virtual: true)

  belongs_to(:user, User)

  timestamps()
end

# teacher.ex

@type t :: %__MODULE__{
  id: integer,
  experience: String.t(),
  
  user: User.t(),
  students: [ Student.t() ],
  subjects: [ Subject.t() ],

  inserted_at: NaiveDateTime.t(),
  updated_at: NaiveDateTime.t()
}

schema "teachers" do

  field(:experience, :integer, default: 1)

  has_one(:user, User)
  has_many(:student, Student)
  has_many(:subject, Subject)

  timestamps()
  end
2 Likes

Custom types should be implemented using Ecto.Type behaviour, so maybe it would be possible to bake this into any new custom type?

There is also a relevant issue in the ejpcmac/typed_struct repo.

3 Likes