In Elixir’s typespec syntax itself, this is not possible. There is no notion in Elixir/Erlang’s typespecs of ‘a couple of keys’ that you add to the specification of another map.
You have the choice of adding one or multiple optional(key_type) => value_type() at the end (where key_type might be a single atom), but a key-value pair like this (e.g. optional(key_type) => value_type() ) does not have a type on its own and cannot be embedded on its own.
Since Elixir’s typespecs have feature-parity with Erlang’s typespecs (there is a little bit of extra syntactic sugar but they are 100% compatible) I do not think a feature like this will ever be added.
As @kokolegorille already pointed out, there are ways to add (runtime) type-checks to your code that might be able to do checks like this.
Currently in the TypeCheck library the notion of a key-value pair also does not exist as its own structure (it is an interesting feature though, PR’s are very welcome ), but it is fully possible to build it yourself in user code and integrate it with the rest of TypeCheck.
So we are able to inject type snippets into other types using quote/unquote.
However, since most types are not valid as values, we also need to have the extra quote around the list. Also, the parentheses around this quote are not optional, because of a parsing precedence edge case in Elixir, making it even more clear that we’re in unexplored waters here: You’re not intended to write this kind of code.
So: This seems to me a clear situation in which the ‘cure’ is worse than the ‘disease’.
The cure indeed looks worse than the disease, but thank you for going into length with this.
Erlang feature-parity be damned, I still think such a basic tool for reusing base structure fields elsewhere should be made available. For instance, my “defextends” macro helps me “extend” elixir structures without needing to copy the “inherited” fields and it proved to be very useful, but I still lack a tool for writing the typespecs.
As for TypeCheck, I don’t think it addresses this problem at all.
In that case I might be misunderstanding your use case. I thought your scenario was that you want to extend the typespec of one struct with some of the fields of the typespec of another struct. TypeCheck is able to facilitate that, and create the final ‘Elixir-compatible’ typespecs to be shown in the documentation and e.g. passed to Dialyzer for you.
But if I’m not correctly paraphrasing your scenario, please do explain .
I took a quick look at the TypeCheck docs and I couldn’t find any mention of extending/inheriting from a structure or a map. Can you please provide a snippet of how it may be done using TypeCheck?
Thank you for this question! I attempted to do it, but I did encounter a problem.
In essence, creating functions that take TypeCheck type-structs as input and generate other type-structs as output is ‘easy’:
def map_union(lhs, rhs) do
lhs_keypairs = extract_keypairs(lhs)
rhs_keypairs = extract_keypairs(rhs)
TypeCheck.Builtin.fixed_map(TypeCheck.Builtin.fixed_list(lhs_keypairs ++ rhs_keypairs))
end
defp extract_keypairs(list) when is_list(list) do
list
end
defp extract_keypairs(list = %TypeCheck.Builtin.FixedList{}) do
list.element_types
end
defp extract_keypairs(map = %TypeCheck.Builtin.FixedMap{}) do
map.keypairs
|> Enum.map(fn {key_atom, val_type} ->
TypeCheck.Builtin.fixed_tuple([TypeCheck.Builtin.literal(key_atom), val_type])
end)
end
and indeed, if we were to add this to the TypeCheck.Builtin module, it would work as intended, and you could e.g. write:
defmodule ReusedKeys do
use TypeCheck
@type! map1 :: %{field1: binary(), field2: binary()}
@type! map2 :: map_union(map1, [field3: binary()])
end
However, currently there is no easy way for a user to make their own custom “type-level” functions available for usage in the types, as type specifications are evaluated in a slightly special context because of implementation reasons.
Thanks! Will be glad to check it out when you add the feature. Please, make the use as neat as possible.
Another thing. As you can imagine, the requirement for a map/struct extension often regards inter-module references e.g.:
defmodule Base do
@type t() :: %__MODULE__{
field1: binary(),
field2: binary()
}
defstruct field1: nil,
field2: nil
end
defmodule Extension do
@type! t() :: subtype( %__MODULE__,
Base.t(), # may be a list if multiple extension
field3: integer(),
field4: integer())
defextends Base,
field3: 1,
field4: 0
end