Search code examples
sorbet

Sorbet signature for nested hash


I have a method that parses YAML files. The returned object is a nested Hash, where the keys are always Strings and the leaf-values are always strings, e.g.

{
    "a" => "foo",
    "b" => {
        "c" => "bar",
        "d" => "baz"
    }
}

I don't know in advance how deep the hash is.

The closest I got to typing the return value was the following signature:

T.any(T::Hash[String,String], T::Hash[String,T::Hash[String, T.untyped]])

This is obviously a bad solution, since it doesn't check anything beneath the second nesting, but the documentation about custom types seems a bit sparse.

Is there any way to type nested hashes, using a custom type, nested types or something similar?


Solution

  • Unfortunately, you won't be able to do much more than what you got to at this point. Even though Shapes are supported, they are an experimental feature.

    If you really want to go with hashes, you could express it as:

    MyType = T.type_alias {T::Hash[String, T.any(String, T::Hash[String, T.untyped])]}
    

    Alternatively, you could use a T::Struct:

    class MyType < T::Struct
      const :key, String
      const :value, T.any(String, MyType)
    end
    

    You'd still have the uncertainty of what the type of the value is, but with flow sensitivity, it should be easy to process the structure:

    class Processor
      extend T::Sig
      
      sig {params(my_object: MyType).returns(String)}
      def process(my_object)
        key = my_object.key
        obj_value = my_object.value # this is needed for flow sensitivity below
        value = case obj_value
        when String
          value
        when MyType
          process(obj_value)
        else
          T.absurd(obj_value) # this makes sure that if you add a new type to `value`, Sorbet will make you handle it
        end
        return "key: #{key}, value: #{value}"
      end
    end