SorbetTyped::Props

An extension of sorbets native props syntax, to make it usable and fully typed in any class. Mainly provides a tapioca dsl compiler to generate the initializer signature when using props outside of T::Struct.

You can use the T::Struct props and const syntax to define attributes on any class.

This should make it easier to create classes with a set of attributes they should be initialized with. Inspiration was the integration of literal properties into phlex, which doesn't really work with sorbet, if you want to have everything fully typed (and not use two runtime typesystems in parallel).

Installation

Install the gem and add to the application's Gemfile by executing:

bundle add sorbet_typed-props

If bundler is not being used to manage dependencies, install the gem by executing:

gem install sorbet_typed-props

Usage

Include the SorbetTyped::Props module in any class you want to have T::Struct-like attributes:

class MyClass
  include SorbetTyped::Props

  prop :my_prop, String
end

my_object = MyClass.new(my_prop: 'foo')

my_object.my_prop # => "foo"
my_object.my_prop = 'bar'

Method Visibility

If you want attributes in your initializer but not be part of your public class interface, you can use ruby's visibility modifiers. Unfortunately I found no better way and did not want to modify sorbet's prop syntax.

class MyClass
  extend T::Sig
  include SorbetTyped::Props

  prop :my_prop, Integer # reader and writer are public
  const :my_const, String # reader ist public, has no writer

  prop :prop_with_private_writer, String # reader is public, writer should be private
  private :prop_with_private_writer= # makes the writer private

  const :my_private_prop, String # reader and writer are private
  private :my_private_prop, :my_private_prop=

  sig { void }
  def foo
    self.prop_with_private_writer = 'foo'
  end

  sig { returns(String) }
  def bar
    self.my_private_prop
  end

  sig { void }
  def baz
    self.my_private_prop = 'baz'
  end
end

my_object = MyClass.new(my_prop: 1, my_const: 'my_const', prop_with_private_writer: 'abc', my_private_prop: 'xyz')

my_object.my_prop # => 1
my_object.my_prop = 2

my_object.my_const # => "my_const"
my_object.my_const = 'abc' # => Setter method `my_const=` does not exist on `MyClass`

my_object.prop_with_private_writer # => "abc"
my_object.prop_with_private_writer = 'foo' # => Non-private call to private method `prop_with_private_writer=` on `MyClass`
my_object.foo
my_object.prop_with_private_writer # => "foo"

my_object.my_private_prop # => Non-private call to private method `my_private_prop` on `MyClass`
my_object.bar # => "xyz"
my_object.my_private_prop = 'baz' # => Non-private call to private method `my_private_prop=` on `MyClass`
my_object.baz
my_object.bar # => "baz"

Putting prop-definition after an access modifier without arguments does not work:

class MyClass
  include SorbetTyped::Props

  private

  prop :my_prop, String # <= still public
end

Development

The project uses mise-en-place as development tool.

After checking out the repo, run mise run setup to install dependencies. Then, run mise test to run the tests. You can also run mise task ls for a list of available tasks.

RSpec is used as test suite. Spec files can and should be placed right beside their associated class files.

Contributing

Bug reports and pull requests are welcome on GitLab at gitlab.com/sorbet_typed/props.