We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Extracting repeated type specs to module-level `@type` declarations
almirsarajcic
Tired of Dialyzer warnings and repetitive type declarations? Clean up your specs by extracting repeated types to the module level. Your code becomes more readable and type changes only need updating in one place.
# Before: Repetitive and hard to maintain
defmodule MyApp.Products do
alias MyApp.Accounts.User
alias MyApp.Products.Product
@spec create_product(User.t(), map(), String.t()) ::
{:ok, Product.t()} | {:error, Ecto.Changeset.t()}
def create_product(user, attrs, category), do: # Implementation
@spec update_product(Product.t(), map(), User.t()) ::
{:ok, Product.t()} | {:error, Ecto.Changeset.t()}
def update_product(product, attrs, user), do: # Implementation
@spec list_products_for_user(User.t(), map()) :: [Product.t()]
def list_products_for_user(user, filters), do: # Implementation
end
# After: Clean and maintainable
defmodule MyApp.Products do
alias MyApp.Accounts.User
alias MyApp.Products.Product
@type attrs :: map()
@type product :: Product.t()
@type result :: {:ok, product()} | {:error, Ecto.Changeset.t()}
@type user :: User.t()
@spec create_product(user(), attrs(), String.t()) :: result()
def create_product(user, attrs, category), do: # Implementation
@spec update_product(product(), attrs(), user()) :: result()
def update_product(product, attrs, user), do: # Implementation
@spec list_products_for_user(user(), attrs()) :: [product()]
def list_products_for_user(user, filters), do: # Implementation
end
Extract semantic types for better documentation:
defmodule MyApp.Orders do
alias MyApp.Accounts.User
alias MyApp.Products.Product
# Extract complex and repeated types
@type order_attrs :: %{
items: [%{product_id: product_id(), quantity: quantity()}],
user_id: user_id()
}
@type price_cents :: non_neg_integer()
@type product_id :: non_neg_integer()
@type quantity :: pos_integer()
@type user_id :: non_neg_integer()
@spec create_order(User.t(), order_attrs()) ::
{:ok, Order.t()} | {:error, Ecto.Changeset.t()}
def create_order(user, attrs) do
# Implementation
end
@spec calculate_total([{Product.t(), quantity()}]) :: price_cents()
def calculate_total(items) do
# Implementation
end
end
For aliased modules, combine with type extraction:
defmodule MyApp.Billing do
alias MyApp.Accounts.User
alias MyApp.Orders.Order
alias MyApp.Payments.Transaction
# Module types - use the alias
@type order :: Order.t()
@type transaction :: Transaction.t()
@type user :: User.t()
# Domain types
@type amount :: Decimal.t()
@type currency :: :usd | :eur | :gbp
@spec charge_user(user(), order(), currency()) ::
{:ok, transaction()} | {:error, String.t()}
def charge_user(user, order, currency) do
# Implementation
end
end
Best practices:
- Extract any type that appears 2+ times in specs
- Group all
@type
definitions at the module top, after aliases - Order types alphabetically for easy scanning
- Create semantic aliases (
user_id
vs genericinteger
) - For opaque external types, reference the struct:
@type session :: %PhoenixTest.Session{}
This pattern makes your code self-documenting and helps Dialyzer provide better error messages when types don’t match.
Copy link
copied to clipboard