union-type

Union types for Ruby. Combine classes into a single type that matches any of them.

Installation

gem "union-type"

Usage

Creating a union type

require "union-type"

StringOrInt = String | Integer        # via Class#|
StringOrInt = UnionType[String, Integer]  # bracket syntax
# => UnionType(Integer | String)

Types are stored in a SortedSet (sorted alphabetically by class name) and deduplicated. Subclasses are dropped when a superclass is already present:

UnionType[Integer, Float, Numeric]
# => UnionType(Numeric)  — Integer and Float are redundant

Matching values

union = String | Integer

union === "hello"   # => true
union === 42        # => true
union === :sym      # => false

"hello".is_a?(union)       # => true
42.kind_of?(union)         # => true
"hi".instance_of?(union)   # => true  (exact class match)
42.instance_of?(union)     # => true
42.instance_of?(Numeric | String)  # => false (42 is not exactly Numeric)

case/when

case value
when String | Integer then "string or int"
when Float            then "float"
end

Combining unions

# Union (|)
(String | Integer) | Float
# => UnionType(Float | Integer | String)

[String, Integer, Float].reduce(:|)
# => UnionType(Float | Integer | String)

# Intersection (&) — returns nil when empty
(String | Integer) & (Integer | Float)
# => UnionType(Integer)

(String | Integer) & Float
# => nil

Subtraction and coverage

union = String | Integer | Float

union.cover?(Integer)  # => true  (exact member)
union.cover?(Numeric)  # => false (superclass, not covered)

numeric = Numeric | String
numeric.cover?(Integer)  # => true  (subclass of Numeric)

Enumerable

UnionType includes Enumerable, yielding classes in sorted order:

union = String | Integer | Float

union.to_a              # => [Float, Integer, String]
union.map(&:name)       # => ["Float", "Integer", "String"]
union.include?(String)  # => true
union.count             # => 3
union.min_by(&:name)    # => Float

Opting out of core extensions

If you don't want Class#|, Object#is_a?, Object#kind_of?, or Object#instance_of? patched, require the no-ext variant instead:

require "union-type-no-ext"

union = UnionType[String, Integer]   # construction still works
union === "hello"                    # => true
"hello".is_a?(union)                 # => false (Object#is_a? not patched)

API reference

Method Description
UnionType.new(*classes) Create a union; raises ArgumentError if empty
UnionType[*classes] Alias for .new
`String \ Integer`
union === value True if value matches any member
union.cover?(klass) True if klass is a member or subclass of a member
`union \ other`
union & other Intersection; returns nil if empty
union.types The underlying SortedSet
value.is_a?(union) Patched is_a? (requires core ext)
value.instance_of?(union) Exact class match against members (requires core ext)

License

MIT