~ views

Enforcing abstract methods with Sorbet


One pattern I’ve seen in Rails projects is raising an error in a subclass if something isn’t implemented. Instead, we can use Sorbet instead to add a type constraint to the abstract method so that subclasses must also implement it.

Here’s the situation:

class BaseTransaction < ApplicationRecord
MIN_AMOUNT = 100

Dynamic constant references are unsupported https://srb.help/5001

def min_amount
raise("Not implemented")

This runtime error can be replaced with a type constraint

end
end

Sorbet does not support dynamic constant references, so we can’t reference something like self.class::MIN_AMOUNT inside the method. Upon searching Github for path:\*.rb self:class::, I found that it’s more common to use another method to return default value, like default_min_amount, which makes more sense compared to overriding constants. Either way, nowadays we can can use Sorbet instead.

To do so, mark a method on the parent class as abstract via the sig{abstract} signature. This type signature means that all subclasses must implement the abstract method and satisfy its return type. Of course, parameter types are also enforceable, and special cases may call for the use of sig{override}.

One thing to note about overriding abstract methods is that the child class must implement the method with at least some of the same parameters and some of the same return types. This is similar to how types can defined in Typescript. I naturally thought type unions worked throughout Sorbet, but I could not create unions of enums. Apparently this is not supported, and instead unions can only be made of subsets of a single enum, such as T.Any(FooEnum:A, FooEnum:B)).

For example, say we have a JobStatus enum and we want to extend it to include a RetryableJobStatus enum.

# typed: true
class JobStatus < T::Enum
# (2) Enum values are declared within an `enums do` block
enums do
Pending = new
Running = new
Finished = new
end
end
class Handler
extend T::Sig
extend T::Helpers
abstract!
sig{abstract.returns(JobStatus)}
def run
end
end

Extending an enum in Sorbet basically isn’t possible. Coming from Typescript, I thought I could use the following type alias, but it didn’t work.

class RetryableJobStatusImpl < T::Enum
# (2) Enum values are declared within an `enums do` block
enums do
RetriesExhaused = new
end
end
RetryableJobStatus = T.type_alias { T.any(JobStatus, RetryableJobStatusImpl) }

Apparently, Sorbet does not support combining enums via T.any, and you would have to access the enum value from the implementation class directly.

class RetryableJobHandler < Handler
extend T::Sig
extend T::Helpers
sig { override.returns(RetryableJobStatus) }
def run
# perform_work...
if retries_exhausted?
# RetryableJobStatus::RetriesExhaused

Resolving constants through type aliases is unsupported (no docs)

RetryableJobStatusImpl::RetriesExhaused

Need to access it from the underlying implementation.

There should be a way to combine enums!

else
JobStatus::Pending
end
end
private
def retries_exhausted?
false
end
end

Therefore instead of extending enums in Sorbet, you must put all the enum values into a single enum directly.

class JobStatus < T::Enum
# (2) Enum values are declared within an `enums do` block
enums do
Pending = new
Running = new
Finished = new
RetriesExhaused = new
end
end
Back to top