TypeSpecs confusion regarding map required/optional

In my code, I have plenty of dynamic maps that I have typed in the following style:

%{optional(String.t()) => integer()}

The idea here being that there are string–integer pairs, but it could also be empty. But is this actually correct? What trips me up is the wording in the TypeSpec documentation:

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

What do “required pairs” and “optional pairs” actually mean? As I see it, there are a couple of interpretations:

  1. Required means that no other key–value types are allowed, but there can be 0…n key–value pairs. Optional means that there can also be other key–value types.
  2. Required means that there is at least one key–value pair matching this spec, optional means 0…n. Both required and optional restrict the key–value types to only match the mentioned.

If we think about option 1, then what is the difference between %{required(a) => b, required(c) => d} and %{required(a) => b, optional(c) => d}? Are they the same? Also, is %{optional(a) => b} completely worthless?

Or if option 2 is true, then I don’t find required very useful at all, because usually I can’t guarantee that dynamic data (for example from the user) has at least one key–value pair. But then how can I type a map that at least has string–integer pairs, but might have others too?

But whichever the case is (or something other than what is listed), I can’t deduce it from the docs, so I’d like to improve them on this part. I just don’t know what is the correct interpretation.

1 Like

required(a) means that there has to be at least 1 key of type a. optional(a) means 0 or more.

2 Likes

Can you cite a code or docs source that states this? Do they have type restriction implications also? I.e. does required(a) => b mean that other key–value types are not allowed? Does optional(a) => b also mean that?

Neither optional nor required say anything about additional keys.

All of them are “additive”. You start with %{} which is the type of the empty map.

If you want to allow 0 or more arbitrary keys, then you need to do %{optional(any) => any}, be aware that %{required(String.t) => :string, optional(any) => any} also includes %{"Foo" => 1}.

2 Likes

If they do not have restrictions on other keys, then a spec of %{optional(a) => b} is worthless, isn’t it? Because it says there might or might not be key–value pairs like these, and there might or might not be other things too. So basically it has no value, except for a human reader.

No. As I said %{} means “no KV pairs allowed, this is the type of the empty map”.

%{optional(a) => b} means, that we can have a map that has 0 or more entries that map a key of type a to a value of type b, but nothing else.

2 Likes

Ah, so…

  • Anything not mentioned in the type is not allowed
  • Required means 1…n pairs
  • Optional means 0…n pairs

Now I feel sad that required is the default, but I realise it’s due to supporting atom literals as keys without having to write required(:atom).

Thanks! :slight_smile:

1 Like