rubystatic-typingsorbet

Sorbet: How do i write a generic factory method?


I have a factory method, which receives a class and returns an instance. In Java, i can express it this way:

Java

class InstantiateMe { }

// Generic factory method
class Factory {

  public static <V> V createInstance(Class<V> type) {
    try {
      return type.newInstance();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}

// returns an instance of the class InstantiateMe
Factory.createInstance(InstantiateMe.class);

When trying to create a similar signature in sorbet, it ends up in an error. The error message of the static checker: T.class_of needs a Class as its argument for the signature of Factory#createInstances.

ruby/sorbet:

# typed: true
class InstantiateMe; end

class Factory
  extend T::Sig

  sig do
    type_parameters(:V)
    params(klass: T.class_of(T.type_parameter(:V)))
    .returns(T.type_parameter(:V))
  end
  def self.createInstance(klass)
    klass.new
  end

end

Factory.createInstance(Factory)

Check online: sorbet.run

Of course i see the point of accepting a class. However, the T.type_parameter(:V) is actually expressing a variable class. So it should be accepted as well. The same way as it is accepted in Java.

However, maybe i am just expressing it wrong. What's the correct way to write a generic factory signature in sorbet?


Solution

  • The way to do this is somewhat hidden in the Sorbet documentation: https://sorbet.org/docs/class-of#tclass_of-applying-type-arguments-to-a-singleton-class-type

    Short example:

    class A; end
    
    class Factory
      extend T::Sig
      sig do 
        type_parameters(:U)
          .params(klass: T::Class[T.type_parameter(:U)])
          .returns(T.type_parameter(:U))
      end
      def self.build(klass)
        klass.new
      end
    end
    
    a = Factory.build(A) # a is an instance of A
    

    If you need to constrain the type parameter to children of a certain type, the type signature needs to be a little different:

    class Base; end
    class A < Base; end # can be passed to factory
    class B; end        # cannot be passed to factory, not a Base
    
    class Factory
      extend T::Sig
      sig do 
        type_parameters(:U)
          .params(klass: T.class_of(Base)[T.all(Base, T.type_parameter(:U))])
          .returns(T.type_parameter(:U))
      end
      def self.build(klass)
        klass.new
      end
    end
    
    a = Factory.build(A)
    b = Factory.build(B) # sorbet static type error