I think maybe the spec could be:
# I am not sure what that first item is in the tuple but if it is an id I might
# make a type like: @type id() :: non_neg_integer() | String.t() assuming that
# maybe the id could be either of those types then I would replace the any() with
# id() in the presence_entry() type.
@type presence_entry() :: {any(), Phoenix.Presence.presence()}
# I am going to make a new type here for the region, but I am making assumptions as I don't know the rest of the code and the type could be something else.
@type region() :: String.t()
@spec presence_by_region([presence_entry()]) :: %{region() => non_neg_integer()}
Dialyzer always assumes the most generic types possible, so since the code provided does not explicitly call anything on the any()
it just assumes this can be anything, as the function will work if you pass anything into it for that parameter. I have found over the years that the more explicit you can be in telling Diazlyer your intentions the better it will type check across the codebase. That is why I recommend making a new type for the any()
s - region()
and id()
.
While Dialyzer isn’t very strict by default you can try to add a few flags to your Dialyzer configuration and just be explicit as possible. Plus I have found that being super explicit in types helps provide documentation on how the code is intended to work.
Another thing I might consider with this code is turning the presence entry into a struct and providing some function to make a Phoenix presence into this more known data structure (please note I am making a few assumptions here around naming, field, and types).
defmodule PresenceUser do
@type id() :: non_neg_integer() | String.t()
@type region() :: String.t()
@type t() :: %__MODULE__{
id: id(),
region: region() | nil
}
defstruct id: nil, region: nil
@spec from_presence_entry(presence_entry()) :: t()
def from_presence_entry({id, presence}) do
region = get_in(presence, [:metas, :region])
%__MODULE__{id: id, region: region}
end
end
## then the new spec for presence_by_region/1
@spec presence_by_region([PresenceUser.t()]) :: %{PresenceUser.region() => non_neg_integer()}
The exact names and types might be different depending on your codebase. I might even move the from_presence_entry/1
up a level in the API.
The reason I would do something like this is that not only is the code explicit to the reader, which is nice, it tells Dialyzer a lot of information, which as the codebase grows will help make Dialyzer seem more strict. However, I know that the community has various degrees of opinions on structs and Dialyzer. While this way of writing typed Elixir requires more typing I found it very useful over the years.
Here’s an example of the Dialyzer flags we set on most of our projects: vintage_net_mobile/mix.exs at main · nerves-networking/vintage_net_mobile · GitHub