[Suggestion] Recommend helper use `Types.string` over `Types::Strict::String`

This was originally a GitHub issue, now moved here for discussion

User Story

As a developer I’d like to be able to access

  • [ ] call type objects with class methods
  • [ ] call class methods even when Dry::Types is aliased to Types

So the code is more readable, & less typing for the developer.

Examples

module Types
 include Dry.Types()
end
# old
drinking_age = Types::Integer.constrained(gt: 21)
# new
drinking_age = Dry::Types.integer.constrained(gt: 21)
# old
email = Dry::Types::String.constrained(
 format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
)
email = Dry::Types.string.constrained(
 format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
)

Not a huge difference, right? But once you’ve alias Types, your code changes:

# from this
drinking_age = Dry::Types::Integer.constrained(gt: 21)
# to this
drinking_age = Types.integer.constrained(gt: 21)
# or if you need more specificity
drinking_age = Types.send('coerible.integer').constrained(gt: 21)

:point_up: 6 characters less, and more readable (in my opinion).

Here’s a working of how I updated my tinder_client gem from using Dry::Types['string'] to Types.string:

   class Updates < Dry::Struct
 
-    attribute :blocks, Dry::Types['array'].of(Dry::Types['string'])
-    attribute :deleted_lists, Dry::Types['array']
-    attribute :goingout, Dry::Types['array']
-    attribute :harassing_messages, Dry::Types['array']
-    attribute :inbox, Dry::Types['array'] do
-      attribute :_id, Dry::Types['string']
-      attribute :match_id, Dry::Types['string']
-      attribute :sent_date, Dry::Types['string']
-      attribute :message, Dry::Types['string']
-      attribute :to, Dry::Types['string']
-      attribute :from, Dry::Types['string']
-      attribute :created_date, Dry::Types['string']
-      attribute :timestamp, Dry::Types['coercible.string']
+    attribute :blocks, Types.array.of(Types.string)
+    attribute :deleted_lists, Types.array
+    attribute :goingout, Types.array
+    attribute :harassing_messages, Types.array
+    attribute :inbox, Types.array do
+      attribute :_id, Types.string
+      attribute :match_id, Types.string
+      attribute :sent_date, Types.string
+      attribute :message, Types.string
+      attribute :to, Types.string
+      attribute :from, Types.string
+      attribute :created_date, Types.string
+      attribute :timestamp, Types.send('coercible.string')
     end
   end
 end

Resources

The way I’m able to accomplish this was by taking the existing snippet from the dry-types docs to create a Types module, then add all the type keys as methods:

spec_helper.rb

require 'dry-types'
require 'dry-struct'

# This automatically transforms keys to symbols when you pass Structs a hash
# that has string keys
class Dry::Struct
  transform_keys(&:to_sym)
end

# The following allow you to access types with Types.<type reference>
#
# ```ruby
# email = Types.string.constrained(
#   format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
# )
# drinking_age = Types.integer.constrained(gt: 21)
# ````
module Types
  include Dry.Types()

  class << self

    # @param String The key of the Dry::Type -- see Dry::Types.type_keys
    def [] (type_key)
      Dry::Types[type_key]
    end

    # This aliases all the Dry::Types keys as class methods
    Dry::Types.type_keys.each do |method_name|
      define_method method_name do
        Dry::Types[method_name]
      end
    end
  end

end

@flash-gordon @solnic please take a look thanks :slight_smile:

I have PR on this: https://github.com/dry-rb/dry-types/pull/362

It’s purely documentation PR

Yes I now realize I should’ve discussed first :slight_smile: but it is what it is.

Defining a module for type constants is the way of using the library. For convenience we can have something like require "dry/types/defaults" (or something) that would define it for you, if doing it manually is an annoying process. The reason why we use constants is because types should be globally accessible constants. It is also very common to define your own types, and for that you want your own namespace too. Using built-in Dry::Types container is actually reserved for some lower-level tasks, like generating types programatically based on some identifiers.

If your really prefer to use methods you can create your own extension, but it will only work with built-in types and defining your own types would make things inconsistent.

I also noticed some confusion so let me clarify:

call class methods even when Dry::Types is aliased to Types

Including Dry.Types into your Types module does not create any alias and Dry::Types does not include type constants. so ie Dry::Types::String does not exist and if you try to access types like that, you will get an exception:

irb(main):001:0> Dry::Types::String
NameError: dry-types does not define constants for default types.
You can access the predefined types with [],
 e.g. Dry::Types["integer"] or generate a module with types using Dry.Types()

When you include Dry.Types it will assign built-in types to constants for you, that’s all it does. This way you have a very clean types namespace w/o any unexpected modules. This is important because dealing with lots of common constant names actually can lead to weird issues if things are not properly namespaced and isolated.

Like I mentioned, if convenient usage in places like one-off scripts or console is the goal, then we can add a helper that defines the default types namespace for you, ie:

require "dry/types/defaults"

# ...and `Types` is already available

Like Nikita said in the related issue, this isn’t something we’d want to have in the library because it would only create confusion about the recommended usage and on top of it it would make accessing types incosistent (methods for built-in types vs accessing custom types in a different way).

Cheers,
P.

2 Likes

That really puts it into perspective for me, thank you. I get what you’re saying: these are meant to be globally accessible constants, and not just a helper layer between that. With that in mind, adding these as methods is not proper organization, and it should be added as a helper, either globally or in whatever context.

Agree :100: - a helper would make it easy, and only requires 1 LoC to get started: require "dry/types/defaults"


Regarding the use of Types.string - personally I love this because it imitates dry-struct another favorite dry library of mine :slight_smile: Do you feel this syntax is too strong a change and is my personal preference? (It could be, I’m genuinely curious)

When I refactored my code from Dry::Types['strict.string'] to Types.string etc, it was absolutely more readable. If there’s any kind of interference that breaks any functionality with dry-types currently, then I can completely understand why it would be a bad change.

Thanks for the reply, looking forward to the feedback

Yeah I guess we should just do it. WDYT @flash-gordon?

It’s not really recommended to use Dry::Types['strict.string'] and a custom types module should be used instead. For me Types::String makes more sense than Types.string because I want to refer to a type explicitly, not through a method call, because there’s literally no reason why it should be hidden behind a method call.

1 Like

@solnic Gotcha. I’m heavily in RSpec doing TDD-style stuff where the syntax is flat like expect(something).to be Something.something. I started using dry-struct because of the constant [''] typing and refactoring, and now with Types shortened it’s even faster - but that’s just my style.

Please please pleaaase let me try a PR for the helper if @flash-gordon is on board :slight_smile:

I like the idea with 'dry/types/defaults'. However, we should add a warning this to be used in application code, not in libraries.

1 Like

@flash-gordon @solnic

Supposing that the docs are updated to recommend require 'dry/types/defaults' – What are everyone’s thoughts on which style to use in examples throughout the docs?

  • Types.string
  • Dry::Types['strict.string']
  • Types['strict.string']
  • Types::Strict::String

Also, currently it’s using Dry::Types['string'] - notice that it’s omitting “strict.” and also adding Dry::

  • Should the examples all be implicit, or explictly strict?

Lmk your thoughts thanks :slight_smile:

Types::String would be fine for strict types and qualified namespaces for other kinds, like Types::Coercible::Integer etc