Elixir: Specifying `key_type` for a map, where `key_type` is an enumerated type

1.2k views Asked by At

I have a function that returns a map which I would like to define a custom type for.

The process of doing it has been pretty easy, except when dealing with key_type from the docs here:

%{required(key_type) => value_type} # map with required pairs of key_type and value_type

For this new type I'm defining, key_type should be any string that is a representation of the current year or a year in the future.

Keys like "2019" or "2025 are good, but keys like "2008" are no good.

I was hoping this would work:

  @type home_sharing_rate :: number
  @type home_sharing_days :: [number]
  @type home_sharing_booking :: { home_sharing_days, home_sharing_rate }

  @type income_year :: "2019" | "2020" | "2021" | "2022" | "2023"
  @type income_month :: "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" | "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec"

  @type earned_home_sharing_income_summary :: %{ required(income_year) => %{ required(income_month ) => [home_sharing_booking] } }

but no such luck. However, I see that Elixir has some built-in types like non_neg_integer() which lead me to believe I could define a type as an enumeration, but now I'm not so sure. Is there any way to do this?

TypeScript has plenty of tools to make this happen so I was kind of hoping Elixir did as well. To me, it seems odd that a type can't be defined as an enumeration of its values, but perhaps the way that Elixir intends types to be used is different than Typescript and I'm just missing something.

1

There are 1 answers

0
Aleksei Matiushkin On BEST ANSWER

Typespecs only handle binaries specified with <<>> syntax, and, unfortunately, only the size and unit are supported. This piece of code would produce a more descriptive error message:

defmodule Foo do
  @typep income_year :: <<"2019"::binary-size(4)>> | <<"2020"::binary-size(4)>>
  @type earned :: %{ required(income_year) => any() }
end

results in

** (CompileError) iex:4: invalid binary specification,
  expected <<_::size>>, <<_::_*unit>>, or <<_::size, _::_*unit>>
  with size and unit being non-negative integers

That said, you might resort to @typep income_year :: <<_::binary-size(4)>> at best.

I would suggest to use the guard instead of type whether you are to expect to deal with this type:

@allowed_years ~w|2019 2020|
Enum.each(@allowed_years, fn year ->
  def do_something(%{unquote(year) => value} = earned) do
    ...
  end
end)

# sink-all clause
def do_something(_), do: raise "Year is not allowed."