Because the values of that hash can be any of those types, you can only call methods on the values that all of those types can handle. Duck typing at compile time can be a little weird to get used to. :-)
It’s possible that you may want something other than a hash to store your values. Maybe you could use a
NamedTuple, which is different from a hash in that the names of each keys the types of their values are known at compile time and are completely separate from one another. For example:
"frequencies" =>  of Float64,
"names" =>  of String,
"count" => 0,
frequencies:  of Float64,
names:  of String,
In the above, the type of all of the hash’s values are
Array(Float64) | Array(String) | Int32, but for the
NamedTuple the type of
Array(Float64), without any of the other types coming into play. Its fully qualified type is actually
NamedTuple(frequencies: Array(Float64), names: Array(String), count: Int32).
The distinction is that a hash is an abstract data type that grows as more keys are added to it but a
NamedTuple is not intended to be abstract. A given
NamedTuple has a very specific use case and it does not grow — you can create new ones from others but you can’t add keys to an existing one. This constraint is actually pretty cool because it means, when accessing a key, the exact offset in memory is known at compile time. It basically becomes an ad hoc object. In fact, I often use them as stepping stones toward building a first-class object.