Intersection Types
This feature is planned for a future release.
Intersection types allow you to combine multiple types into one, creating a type that has all the properties and methods of each combined type. Think of intersection types as "AND" relationships—a value must satisfy all types in the intersection.
Understanding Intersection Types
While union types represent "either-or" relationships (A | B means "A OR B"), intersection types represent "and" relationships (A & B means "A AND B").
Union vs Intersection
# Union type: value can be String OR Integer
type StringOrInt = String | Integer
value1: StringOrInt = "hello" # OK
value2: StringOrInt = 42 # OK
# Intersection type: value must have properties of BOTH types
type NamedAndAged = Named & Aged
# Must have both name (from Named) and age (from Aged)
Basic Intersection Syntax
The intersection operator is &:
type Combined = TypeA & TypeB & TypeC
Combining Interfaces
The most common use of intersection types is combining interfaces:
# Define individual interfaces
interface Named
def name: String
end
interface Aged
def age: Integer
end
interface Contactable
def email: String
def phone: String
end
# Combine interfaces with intersection
type Person = Named & Aged
type Employee = Named & Aged & Contactable
# A class implementing the intersection must implement all interfaces
class User
implements Named, Aged
@name: String
@age: Integer
def initialize(name: String, age: Integer): void
@name = name
@age = age
end
def name: String
@name
end
def age: Integer
@age
end
end
# User can be used as Person type
user: Person = User.new("Alice", 30)
puts user.name # OK: Named interface
puts user.age # OK: Aged interface
Mixing Types and Interfaces
You can combine interfaces with class types:
# Base class
class Entity
@id: Integer
def initialize(id: Integer): void
@id = id
end
def id: Integer
@id
end
end
# Interface
interface Timestamped
def created_at: Time
def updated_at: Time
end
# Intersection of class and interface
type TimestampedEntity = Entity & Timestamped
# Implementation must extend Entity AND implement Timestamped
class User < Entity
implements Timestamped
@name: String
@created_at: Time
@updated_at: Time
def initialize(id: Integer, name: String): void
super(id)
@name = name
@created_at = Time.now
@updated_at = Time.now
end
def created_at: Time
@created_at
end
def updated_at: Time
@updated_at
end
end
# User satisfies the intersection type
user: TimestampedEntity = User.new(1, "Alice")
puts user.id # From Entity class
puts user.created_at # From Timestamped interface
Practical Examples
Mixins Pattern
Intersection types work well with Ruby's mixin concept:
# Define capability interfaces
interface Serializable
def to_json: String
def self.from_json(json: String): self
end
interface Validatable
def valid?: Boolean
def errors: Array<String>
end
interface Persistable
def save: Boolean
def delete: Boolean
end
# Combine capabilities as needed
type Model = Serializable & Validatable & Persistable
# A full-featured model class
class Article
implements Serializable, Validatable, Persistable
@title: String
@content: String
@errors: Array<String>
def initialize(title: String, content: String): void
@title = title
@content = content
@errors = []
end
def to_json: String
"{ \"title\": \"#{@title}\", \"content\": \"#{@content}\" }"
end
def self.from_json(json: String): Article
# Parse JSON and create instance
Article.new("Title", "Content")
end
def valid?: Boolean
@errors = []
@errors.push("Title cannot be empty") if @title.empty?
@errors.push("Content cannot be empty") if @content.empty?
@errors.empty?
end
def errors: Array<String>
@errors
end
def save: Boolean
return false unless valid?
# Save to database
true
end
def delete: Boolean
# Delete from database
true
end
end
# Article satisfies Model intersection type
article: Model = Article.new("Hello", "World")
puts article.to_json # Serializable
puts article.valid? # Validatable
article.save # Persistable
Repository Pattern
interface Identifiable
def id: Integer | String
end
interface Timestamped
def created_at: Time
def updated_at: Time
end
interface SoftDeletable
def deleted?: Boolean
def deleted_at: Time | nil
end
# Different combinations for different needs
type BaseEntity = Identifiable & Timestamped
type DeletableEntity = Identifiable & Timestamped & SoftDeletable
class Repository<T: BaseEntity>
@items: Array<T>
def initialize: void
@items = []
end
def find(id: Integer | String): T | nil
@items.find { |item| item.id == id }
end
def all: Array<T>
@items.dup
end
def recent(limit: Integer = 10): Array<T>
@items.sort_by { |item| item.created_at }.reverse.take(limit)
end
end
class SoftDeleteRepository<T: DeletableEntity> < Repository<T>
def all: Array<T>
@items.reject { |item| item.deleted? }
end
def with_deleted: Array<T>
@items.dup
end
def only_deleted: Array<T>
@items.select { |item| item.deleted? }
end
end
Event System
interface Event
def event_type: String
def timestamp: Time
end
interface Cancellable
def cancelled?: Boolean
def cancel: void
end
interface Prioritized
def priority: Integer
end
# Different event types with different capabilities
type BasicEvent = Event
type CancellableEvent = Event & Cancellable
type PrioritizedCancellableEvent = Event & Cancellable & Prioritized
class UserClickEvent
implements Event
@event_type: String
@timestamp: Time
def initialize: void
@event_type = "user_click"
@timestamp = Time.now
end
def event_type: String
@event_type
end
def timestamp: Time
@timestamp
end
end
class NetworkRequestEvent
implements Event, Cancellable
@event_type: String
@timestamp: Time
@cancelled: Boolean
def initialize: void
@event_type = "network_request"
@timestamp = Time.now
@cancelled = false
end
def event_type: String
@event_type
end
def timestamp: Time
@timestamp
end
def cancelled?: Boolean
@cancelled
end
def cancel: void
@cancelled = true
end
end
class CriticalAlertEvent
implements Event, Cancellable, Prioritized
@event_type: String
@timestamp: Time
@cancelled: Boolean
@priority: Integer
def initialize(priority: Integer): void
@event_type = "critical_alert"
@timestamp = Time.now
@cancelled = false
@priority = priority
end
def event_type: String
@event_type
end
def timestamp: Time
@timestamp
end
def cancelled?: Boolean
@cancelled
end
def cancel: void
@cancelled = true
end
def priority: Integer
@priority
end
end
# Event handlers for different event types
def handle_basic_event(event: BasicEvent): void
puts "Event: #{event.event_type} at #{event.timestamp}"
end
def handle_cancellable_event(event: CancellableEvent): void
if event.cancelled?
puts "Event #{event.event_type} was cancelled"
else
puts "Processing #{event.event_type}"
end
end
def handle_priority_event(event: PrioritizedCancellableEvent): void
puts "Priority #{event.priority}: #{event.event_type}"
event.cancel if event.priority < 5
end
Intersection with Generics
Intersection types can be combined with generics:
# Generic type with intersection constraint
def process<T: Serializable & Validatable>(item: T): Boolean
if item.valid?
json = item.to_json
# Send to API
true
else
puts "Validation errors: #{item.errors.join(', ')}"
false
end
end
# Collection that requires multiple capabilities
class ValidatedCollection<T: Identifiable & Validatable>
@items: Array<T>
def initialize: void
@items = []
end
def add(item: T): Boolean
if item.valid?
@items.push(item)
true
else
false
end
end
def find(id: Integer | String): T | nil
@items.find { |item| item.id == id }
end
def all_valid: Array<T>
@items.select { |item| item.valid? }
end
def all_invalid: Array<T>
@items.reject { |item| item.valid? }
end
end
Type Guards and Narrowing
Intersection types work with type narrowing:
interface Animal
def speak: String
end
interface Swimmable
def swim: void
end
interface Flyable
def fly: void
end
type Duck = Animal & Swimmable & Flyable
class DuckImpl
implements Animal, Swimmable, Flyable
def speak: String
"Quack!"
end
def swim: void
puts "Swimming..."
end
def fly: void
puts "Flying..."
end
end
def test_duck(animal: Animal): void
puts animal.speak
# Type narrowing with responds_to?
if animal.responds_to?(:swim) && animal.responds_to?(:fly)
# Here animal is treated as Duck (Animal & Swimmable & Flyable)
duck = animal as Duck
duck.swim
duck.fly
end
end
Conflicts and Resolution
When intersection types have conflicting members, the more specific type wins:
interface HasName
def name: String
end
interface HasOptionalName
def name: String | nil
end
# The intersection requires the more restrictive type
type Person = HasName & HasOptionalName
# person.name must be String (not String | nil)
# because String is more specific than String | nil
class User
implements HasName, HasOptionalName
@name: String
def initialize(name: String): void
@name = name
end
# Must return String to satisfy both interfaces
def name: String
@name
end
end
Best Practices
1. Compose Small, Focused Interfaces
# Good: Small, single-responsibility interfaces
interface Identifiable
def id: Integer
end
interface Named
def name: String
end
interface Timestamped
def created_at: Time
end
type Entity = Identifiable & Named & Timestamped
# Less good: Large, monolithic interface
interface Entity
def id: Integer
def name: String
def created_at: Time
def updated_at: Time
def save: Boolean
def delete: Boolean
# Too many responsibilities
end
2. Use Meaningful Names
# Good: Clear what the intersection represents
type AuditedEntity = Entity & Auditable
type SerializableModel = Model & Serializable
# Less good: Generic names
type TypeA = Interface1 & Interface2
type Combined = Foo & Bar
3. Don't Over-Complicate
# Good: Reasonable number of intersections
type FullModel = Identifiable & Timestamped & Validatable
# Potentially problematic: Too many intersections
type SuperType = A & B & C & D & E & F & G & H
# Hard to implement and understand
4. Document Intent
# Good: Comment explains why intersection is needed
# Represents entities that can be both serialized and cached
type CacheableEntity = Serializable & Identifiable
# Cache implementation
class Cache<T: CacheableEntity>
@store: Hash<Integer | String, String>
def set(entity: T): void
@store[entity.id] = entity.to_json
end
def get(id: Integer | String): String | nil
@store[id]
end
end
Common Patterns
Builder Pattern
interface Buildable
def build: self
end
interface Validatable
def valid?: Boolean
end
interface Resettable
def reset: void
end
type CompleteBuilder = Buildable & Validatable & Resettable
class FormBuilder
implements Buildable, Validatable, Resettable
@fields: Hash<String, String>
@errors: Array<String>
def initialize: void
@fields = {}
@errors = []
end
def add_field(name: String, value: String): self
@fields[name] = value
self
end
def build: self
self
end
def valid?: Boolean
@errors = []
@errors.push("No fields") if @fields.empty?
@errors.empty?
end
def reset: void
@fields = {}
@errors = []
end
end
State Machine
interface State
def name: String
end
interface Transitionable
def can_transition_to?(state: String): Boolean
def transition_to(state: String): void
end
interface Observable
def on_enter: void
def on_exit: void
end
type ManagedState = State & Transitionable & Observable
class WorkflowState
implements State, Transitionable, Observable
@name: String
@allowed_transitions: Array<String>
def initialize(name: String, allowed_transitions: Array<String>): void
@name = name
@allowed_transitions = allowed_transitions
end
def name: String
@name
end
def can_transition_to?(state: String): Boolean
@allowed_transitions.include?(state)
end
def transition_to(state: String): void
if can_transition_to?(state)
on_exit
# Perform transition
on_enter
else
raise "Invalid transition from #{@name} to #{state}"
end
end
def on_enter: void
puts "Entering state: #{@name}"
end
def on_exit: void
puts "Exiting state: #{@name}"
end
end
Limitations
Cannot Intersect Primitive Types
# This doesn't make sense - a value can't be both String AND Integer
# type Impossible = String & Integer # Would be empty type
# Intersection makes sense for structural types (interfaces, classes)
type Valid = Interface1 & Interface2
Implementation Requirements
# When using intersection, implementation must satisfy ALL parts
type Complete = Interface1 & Interface2 & Interface3
class MyClass
# Must implement ALL of: Interface1, Interface2, Interface3
implements Interface1, Interface2, Interface3
# ...
end
Next Steps
Now that you understand intersection types, explore:
- Union Types for "or" type relationships
- Type Aliases for naming complex intersections
- Interfaces to create the building blocks for intersections
- Conditional Types for types that depend on conditions