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 < ApplicationRecordMIN_AMOUNT = 100Dynamic constant references are unsupported https://srb.help/5001
def min_amountraise("Not implemented")This runtime error can be replaced with a type constraint
endend
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: trueclass JobStatus < T::Enum# (2) Enum values are declared within an `enums do` blockenums doPending = newRunning = newFinished = newendendclass Handlerextend T::Sigextend T::Helpersabstract!sig{abstract.returns(JobStatus)}def runendend
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` blockenums doRetriesExhaused = newendendRetryableJobStatus = 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 < Handlerextend T::Sigextend T::Helperssig { override.returns(RetryableJobStatus) }def run# perform_work...if retries_exhausted?# RetryableJobStatus::RetriesExhausedResolving constants through type aliases is unsupported (no docs)
RetryableJobStatusImpl::RetriesExhausedNeed to access it from the underlying implementation.
There should be a way to combine enums!
elseJobStatus::Pendingendendprivatedef retries_exhausted?falseendend
Therefore instead of extending enums in Sorbet, you must put all the enum values into a single enum directly.
Back to top
class JobStatus < T::Enum# (2) Enum values are declared within an `enums do` blockenums doPending = newRunning = newFinished = newRetriesExhaused = newendend