# Suggestion: Simplify Dry::Structs type system

**URL:** https://discourse.dry-rb.org/t/suggestion-simplify-dry-structs-type-system/1377
**Category:** Uncategorized
**Created:** [November 1, 2021, 1:44pm UTC](https://discourse.dry-rb.org/t/suggestion-simplify-dry-structs-type-system/1377 "2021-11-01T13:44:40Z")
**Posts on this page:** 1
**Page:** 1

<div class="post-metadata">

### Author: ![oleander](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.dry-rb.org/oleander/32/721_2.png) [@oleander](https://discourse.dry-rb.org/u/oleander)
#### Post date: [November 1, 2021, 1:44pm UTC](https://discourse.dry-rb.org/t/suggestion-simplify-dry-structs-type-system/1377/1 "2021-11-01T13:44:40Z")

</div>

I’ve been trying to get my colleagues to switch over from plain classes to dry struct for a while and the biggest obstacle for most people seems to be its dependence on Dry::Types. Abstract data types aren’t very common in Ruby and RBS introduced in Ruby 3.0 is still very new to most developers. There also seems to be a lot of confusion in regards to its syntax, i.e `Types::Array` vs `Types.Array(a type)`. To a beginner, `Array()` looks like a module that now takes a type. Even when I try to explain the difference, the next question is often: ff it is a method, why is it capitalized?

I would suggest a type system built on top of `Dry::Types` that allows the developer to define type constraints using native Ruby classes

A few examples would be

- `String` == `Types::String`.
- `[String]` == `Types.Array(Types::String)`.
- `String, Symbol` == `Types::String | Types::Symbol`.
- `[String, Symbol]` == `Types.Array(Types::String | Types::Symbol)`.
- `MyClass` == ` Types.Constructor(MyClass, &MyClass.method(:new))`.
- `{ Symbol => [String] }` == `Types::Hash.map(Types::Symbol, Types.Array(Types::String))`

Dry initializer does this to some extent when wrapping array types

```ruby
option :emails, [[proc(&:to_s)]]

```

This could fairly easily be accomplished by using refinements to recursively build the type (see `extension.rb` below) without affecting the existing behavior

```ruby
module Extension
  module Type
    module Types
      include Dry::Types()
    end

    refine String do
      # Converts dry type references into a type
      #
      # @example A strict string into a type
      # type = "strict.string".to_type
      #
      # type.valid?("string") # => true
      # type.valid?(:symbol) # => false
      #
      # @example A (strict) symbol into a type
      # type = "symbol".to_type
      #
      # type.valid?(:symbol) # => true
      # type.valid?("string") # => false
      #
      # @return [Dry::Types::Type]
      # @raise [ArgumentError] if the type is not a valid type
      def to_type
        Dry::Types[self]
      rescue Dry::Container::Error
        raise ArgumentError, "Type reference [#{inspect}] not found in Dry::Types"
      end
    end

    refine Object do
      def to_type
        raise ArgumentError, "[#{self}] cannot be converted into a type"
      end
    end

    refine Dry::Types::Type do
      # Dry::Types::Type is already a type in itself
      # Used to streamline the API for all objects
      #
      # @example Dry type to dry type
      # type = Dry::Types['string'].to_type
      #
      # type.valid?("string") # => true
      # type.valid?(:string) # => false
      #
      # @return [Dry::Types::Type]
      alias_method :to_type, :itself
    end

    refine Module do
      # Ensures passed value includes module
      #
      # @example Check for enumerable values
      # type = Enumerable.to_type
      #
      # type.valid?([]) # => true
      # type.valid?({}) # => true
      # type.valid?(nil) # => false
      #
      # @return [Dry::Types::Constrained]
      def to_type
        Types::Any.constrained(type: self)
      end
    end

    refine Class do
      # Wrapps class in a type constructor using ::new as initializer
      #
      # @example With a custom class
      # type = Struct.new(:value).to_type
      #
      # type.valid?('value') # => true
      # type.valid? # => false
      #
      # @example With a native Ruby class
      # type = String.to_type
      #
      # type.valid?('value') # => true
      # type.valid?(:symbol) # => false
      #
      # @example With an instance of the class
      # Person = Struct.new(:name)
      #
      # type = Person.to_type
      #
      # type.valid?('value') # => true
      # type.valid?(Person.new('John')) # => true
      # type.valid? # => false
      #
      # @example With a class without constructor args
      # type = Mutex.to_type
      #
      # type.valid? # => true
      # type.valid?('value') # => false
      #
      # @return [Dry::Types::Constructor]
      def to_type
        Types.const_get(name).then do |maybe_type|
          maybe_type == self ? to_constructor : maybe_type
        end
      rescue NameError, TypeError
        to_constructor
      end

      private

      def to_constructor
        Types.Instance(self) | Types.Constructor(self, method(:new))
      end
    end

    refine Hash do
      # Recursively creates a hash type from {keys} & {values}
      #
      # @example With dynamic key and static value
      # type = { String => 'value' }
      #
      # type.valid?('string' => 'value') # => true
      # type.valid?(symbol: 'value') # => false
      # type.valid?('string' => 'other') # => false
      #
      # @example With dynamic key and value
      # type = { String => Enumerable }.to_type
      #
      # type.valid?('string' => []) # => true
      # type.valid?(symbol: []) # => false
      # type.valid?('string' => :symbol) # => false
      #
      # @return [Dry::Types::Constrained, Dry::Types::Map]
      def to_type
        return Types::Hash if empty?

        map { |k, v| Types::Hash.map(k.to_type, v.to_type) }.reduce(:|)
      end
    end

    refine Array do
      # Recursively creates an array type from {self}
      #
      # @example With member type
      # type = [String].to_type
      #
      # type.valid?(['string']) # => true
      # type.valid?([:symbol]) # => false
      #
      # @example Without member type
      # type = [].to_type
      #
      # type.valid?(['anything']) # => true
      # type.valid?('not-an-array') # => false
      #
      # @example With nested members
      # type = [[String]].to_type
      #
      # type.valid?([['string']]) # => true
      # type.valid?([[:symbol]]) # => false
      # type.valid?(['string']) # => false
      #
      # @example With combined types
      # type = [String, Symbol].to_type
      #
      # type.valid?(['string', :symbol]) # => true
      # type.valid?(['string']) # => true
      # type.valid?([:symbol]) # => true
      # type.valid?([]) # => true
      # type.valid?(:symbol) # => false
      #
      # @return [Dry::Types::Constrained]
      def to_type
        return Types::Array if empty?

        Types.Array(map(&:to_type).reduce(:|))
      end
    end
  end
end

```

```ruby
class Example < Dry::Struct
  attribute :field, String
  attribute :field, Float, Integer
  attribute :field, Value('id')
  attribute :field, String, default: 'John Doe'
  attribute :field, String, min_size: 3
  attribute :field, Coercible::String
  attribute :field, Enumerable
  attribute :field, OpenStruct
  attribute :field, [String]
  attribute :field, [String, Symbol]
  attribute :field, [Value(:id), Value(:null)]
  attribute :field, { String => Hash }
  attribute :field, { String => Value('value') }
  attribute :field, { String => [Value(:id), Value(:null)] }
  attribute :field, { [String] => String }
  attribute :field, { [[Coercible::Symbol], String, Types::Symbol] => [OpenStruct, :id] }

```

What do you think, could this improve the user experience?
