Introduction
dry-types
is a simple and extendable type system for Ruby; useful for value coercions, applying constraints, defining complex structs or value objects and more. It was created as a successor to Virtus.
Example usage
require 'dry-types'
require 'dry-struct'
module Types
include Dry.Types()
end
User = Dry.Struct(name: Types::String, age: Types::Integer)
User.new(name: 'Bob', age: 35)
# => #<User name="Bob" age=35>
See Built-in Types for a full list of available types.
By themselves, the basic type definitions like Types::String
and Types::Integer
don't do anything except provide documentation about which type an attribute is expected to have. However, there are many more advanced possibilities:
Strict
types will raise an error if passed an attribute of the wrong type:
class User < Dry::Struct
attribute :name, Types::Strict::String
attribute :age, Types::Strict::Integer
end
User.new(name: 'Bob', age: '18')
# => Dry::Struct::Error: [User.new] "18" (String) has invalid type for :age
Coercible
types will attempt to convert an attribute to the correct class using Ruby's built-in coercion methods:
class User < Dry::Struct
attribute :name, Types::Coercible::String
attribute :age, Types::Coercible::Integer
end
User.new(name: 'Bob', age: '18')
# => #<User name="Bob" age=18>
User.new(name: 'Bob', age: 'not coercible')
# => ArgumentError: invalid value for Integer(): "not coercible"
- Use
.optional
to denote that an attribute can benil
(see Optional Values):
class User < Dry::Struct
attribute :name, Types::String
attribute :age, Types::Integer.optional
end
User.new(name: 'Bob', age: nil)
# => #<User name="Bob" age=nil>
# name is not optional:
User.new(name: nil, age: 18)
# => Dry::Struct::Error: [User.new] nil (NilClass) has invalid type for :name
# keys must still be present:
User.new(name: 'Bob')
# => Dry::Struct::Error: [User.new] :age is missing in Hash input
- Add custom constraints (see Constraints):
class User < Dry::Struct
attribute :name, Types::Strict::String
attribute :age, Types::Strict::Integer.constrained(gteq: 18)
end
User.new(name: 'Bob', age: 17)
# => Dry::Struct::Error: [User.new] 17 (Fixnum) has invalid type for :age
- Add custom metadata to a type:
class User < Dry::Struct
attribute :name, Types::String
attribute :age, Types::Integer.meta(info: 'extra info about age')
end
User.schema.key(:age).meta
# => {:info=>"extra info about age"}
- Pass values directly to
Dry::Types
without creating an object using[]
:
Types::Strict::String["foo"]
# => "foo"
Types::Strict::String["10000"]
# => "10000"
Types::Coercible::String[10000]
# => "10000"
Types::Strict::String[10000]
# Dry::Types::ConstraintError: 1000 violates constraints
Features
- Support for constrained types
- Support for optional values
- Support for default values
- Support for sum types
- Support for enums
- Support for hash type with type schemas
- Support for array type with members
- Support for arbitrary meta information
- Support for typed struct objects via dry-struct
- Types are categorized, which is especially important for optimized and dedicated coercion logic
- Types are composable and reusable objects
- No const-missing magic and complicated const lookups
- Roughly 6-10 x faster than Virtus
Use cases
dry-types
is suitable for many use-cases, for example:
- Value coercions
- Processing arrays
- Processing hashes with explicit schemas
- Defining various domain-specific information shared between multiple parts of your application
- Annotating objects
Other gems using dry-types
dry-types
is often used as a low-level abstraction. The following gems use it already: