How to define a signature for a hash with attributes in Sorbet?
Asked Answered
S

1

5

(Note that this isn't reproducible on sorbet.run, it's only reproducible with a local copy of Sorbet as far as I can tell)

I was hoping I could use the Typed Structs feature to create a method signature where one of the parameters is an options hash, but this doesn't work:

# typed: true
require 'sorbet-runtime'
extend T::Sig

class OptionsStruct < T::Struct
  prop :x, Integer, default: 1
end

sig { params(options: OptionsStruct).void }
def method(options)
  puts options.x
end

# This works
method(OptionsStruct.new({x: 2}))

# This causes the typechecker to throw.
method({x: 2})

Essentially, when you typecheck this file it complains about passing a hash in, when a Struct is expected. My question is: how can I define a valid signature for a hash that has specific parameters? Structs clearly aren't working here. While I haven't tried Shapes, according to the docs they're very limited, so I'd prefer not to use them if possible.

The documentation on generics mentions Hashes but seems to suggest they can only be used if the hash's keys and values are all of the same types (e.g. Hash<Symbol, String> requires that all keys be Symbols and all values be Strings), and doesn't provide any way (as far as I know) to define a hash with specific keys.

Thanks!

Sekyere answered 30/6, 2019 at 20:58 Comment(2)
And to be clear: using Hash technically works in the signature, but then when you use options.x in the method it says x isn't defined on the Hash.Sekyere
options.x isn't a valid accessor for a hash... whoops. It should be options[:x].Sekyere
B
7

Essentially, you have to choose to go one of various ways, (three which you've already mentioned):

  1. Use a T::Hash[KeyType, ValueType]. This allows you to use the {} syntax when calling a method that takes it as a param, but forces you to use the same type of key and value for every entry.
  2. Use a T::Hash[KeyType, Object]. This is a bit more flexible on the type of the value... but you loose type information.
  3. Use a T::Hash[KeyType, T.any(Type1, Type2, ...). This is a middle ground between 1 and 2.
  4. Use shapes. As the docs say, the functionality might change and are experimental. It's the nicest way to model something like this without imposing the use of the T::Struct to the caller:
sig { params(options: {x: Integer}).void }
def method(options)
  puts options[:x]
end
  1. Use a T::Struct, like you did. This forces you to call the method with MyStruct.new(prop1: x, prop2: y, ...)

All of them are valid, with 4 and 5 being the ones that give you the most type safety. Of the two, 4 is the most flexible on the caller, but 5 is the one that you know Sorbet is not going to change support in the short/medium term.

Bohemia answered 30/6, 2019 at 21:11 Comment(2)
Interestingly, in your example for #4 the typechecker seems to fail on options.x (Method x does not exist on T::Hash[T.untyped, T.untyped] component of {x: Integer}), but options[:x] is fine.Sekyere
Ooops, sorry, you are right, was copy&pasting the method from your example and forgot to change to the shape. Fixed now!Bohemia

© 2022 - 2024 — McMap. All rights reserved.