Connor Shea
Connor Shea

Reputation: 870

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

(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!

Upvotes: 5

Views: 4905

Answers (1)

marianosimone
marianosimone

Reputation: 3606

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.

Upvotes: 7

Related Questions