1

all! I want to define a generic result data type, which is a union of a generic success type and a generic failure type. The same thing in TS looks like this:

type Success<T> = {
    value: T
}

type Failure<E> = {
    error: E
}

type Result<T, E> = Success<T> | Failure<E>

or in Rust like this:

enum Result<T, E> {
   Ok(T),
   Err(E),
}

But I, unfortunately, couldn't find a way to do that using Sorbet type annotations. Is that even possible?

Thank you very much.

The closest thing I've found was type definitions for the gem dry-monads but it's not really what I want because it looks like a hack because both Success and Failure classes should redefine both type_members.

Explanation

If you take a look at this example: https://gist.github.com/woarewe/f4f3ee502f35c4c0d097695a52031b14 My goal is to define a signature that looks like this:

sig { params(value: Integer).returns(Core::Type::Result[Integer, String]) }

But it seems that it's not possible cause it seems that there is no way to pass a generic type from one class to another.

The only workaround I found is to build a union with specific types right in the function definition:

sig { params(value: Integer).returns(T.any(Core::Type::Success[Integer], Core::Type::Failure[String])) }
def zero?(value)
  if value.zero?
    Core::Type::Success.new(value)
  else
    Core::Type::Failure.new("It is not zero")
  end
end

Final solution looks like this

# typed: strict
# frozen_string_literal: true

module Core
  module Type
    class Success
      extend T::Sig
      extend T::Generic

      ValueType = type_member

      sig { returns(ValueType) }
      attr_reader :value

      sig { params(value: ValueType).void }
      def initialize(value)
        @value = value
      end
    end

    class Failure
      extend T::Sig
      extend T::Generic

      ErrorType = type_member

      sig { returns(ErrorType) }
      attr_reader :error

      sig { params(error: ErrorType).void }
      def initialize(error)
        @error = error
      end
    end
  end
end

extend T::Sig

sig { params(value: Integer).returns(T.any(Core::Type::Success[Integer], Core::Type::Failure[String])) }
def zero?(value)
  if value.zero?
    Core::Type::Success.new(value)
  else
    Core::Type::Failure.new("It is not zero")
  end
end

result = zero?(0)
case result
when Core::Type::Success
  p result.value
when Core::Type::Failure
  p result.error
end

1 Answers1

-1

I've recently helped developing a gem that implements exactly what you're looking for. https://github.com/maxveldink/sorbet-result

Using this gem, you could rewrite your code as follows:

sig { params(value: Integer).returns(Typed::Result[Integer, String]) }
def zero?(value)
  if value.zero?
    Typed::Success.new(value)
  else
    Typed::Failure.new("It is not zero")
  end
end

result = zero?(0)
if result.success?
  p result.payload
else
  p result.error
end

The gem also supports chaining and other nice features.

iMacTia
  • 671
  • 4
  • 8