Generics: Type Constraints

A type constraint on a generic type parameter indicates a requirement that a type must fulfill in order to be accepted as a type argument for that type parameter. (For example, it might have to be a given class type or a subtype of that class type, or it might have to implement a given interface.)

A constraint can have one of three forms:

  • T as sometype, meaning that T must be a subtype of sometype
  • T super sometype, meaning that T must be a supertype of sometype
  • T = sometype, meaning that T must be equivalent to sometype. (This is like saying both T as sometype and T super sometype.)

Consider the following example in which function max_val has one type parameter, T, and that has a constraint, num:

<?hh // strict

namespace Hack\UserDocumentation\Generics\TypeConstraints\Examples\MaxVal;

function max_val<T as num>(T $p1, T $p2): T {
  return $p1 > $p2 ? $p1 : $p2;
}

<<__EntryPoint>>
function main(): void {
  echo "max_val(10, 20) = " . max_val(10, 20) . "\n";
  echo "max_val(15.6, -20.78) = " . max_val(15.6, -20.78) . "\n";
}
Output
max_val(10, 20) = 20
max_val(15.6, -20.78) = 15.6

Without the num constraint, the expression $p1 > $p2 is ill-formed, as a greater-than operator is not defined for all types. By constraining the type of T to num, we limit T to being an int or float, both of which do have that operator defined.

Unlike an as constraint, T super U asserts that T must be a supertype of U. This kind of constraint is rather exotic, but solves an interesting problem encountered when multiple types "collide". Here is an example of how it's used on method concat in the library interface type ConstVector:

interface ConstVector<+T> {
  public function concat<Tu super T>(ConstVector<Tu> $x): ConstVector<Tu>;
  // ...
}

Consider the case in which we call concat to concatenate a Vector<float> and a Vector<int>. As these have a common supertype, num, the super constraint allows the checker to determine that num is the inferred type of Tu.

Now, while a type parameter on a class can be annotated to require that it is a subtype or supertype of a particular type, for generic parameters on classes, constraints on the type parameters can be assumed in any method in the class. But sometimes some methods want to use some features of the type parameter, and others want to use some different features, and not all instances of the class will satisfy all constraints. This can be done by specifying constraints that are local to particular methods. For example:

class MyWidget<Telem> {
  public function showIt(): void where Telem as IPrintable { ... }
  public function countIt(): int where Telem as ICountable { ... }
}

Constraints can make use of the type parameter itself. They can also make use of generic type parameters on the method. For example:

class MyList<T> {
  public function flatten<Tu>(): MyList<Tu> where T = MyList<Tu> { ... }
}

Here we might create a list of lists of int, of type MyList<MyList<int>>, and then invoke flatten on it to get a MyList<int>. Here's another example:

class MyList<T> {
  public function compact<Tu>(): MyList<Tu> where T = ?Tu { ... }
}

A where constraint permits multiple constraints supported; just separate the constraints with commas. For example:

class SomeClass<T> {
  function foo(T $x) where T as MyInterface, T as MyOtherInterface
}

If a method overrides another method that has declared where constraints, it's necessary to redeclare those constraints, but only if they are actually used by the overriding method. (It's valid, and reasonable, to require less of the overriding method.)