Shapes: Subtyping

By default, shape types must exactly match:

<?hh // strict

namespace Hack\UserDocumentation\Shapes\Subtyping\Examples\Implicit;

type Point = shape('x' => int, 'y' => int);

function get_3d_point(): Point {
  // Invalid return type (Typing[4166])
  //   The field 'z' is not defined in this shape type, and this shape type does not allow unknown fields.
  //   The field 'z' is set in the shape.
  return shape('x' => 123, 'y' => 456, 'z' => 789);
}

Shapes also support structural subtyping (also known as implicit subtypes) - that is, you can mark a shape as allowing extra fields to be defined. You can enable this behavior for a shape by adding ... to the end of the field declaration:

<?hh // strict

namespace Hack\UserDocumentation\Shapes\Subtyping\Examples\AllowUndefined;

type Point = shape(
  'x' => int,
  'y' => int,
  ... // allow undefined fields
);

function get_3d_point(): Point {
  // No error
  return shape('x' => 123, 'y' => 456, 'z' => 789);
}

It's best to avoid this where possible - it can lead to hard to debug problems, especially when combined with optional fields:

<?hh // strict

namespace Hack\UserDocumentation\Shapes\Subtyping\Examples\UndefinedAndOptional;

type MyShape = shape(
  'foo' => int,
  'bar' => int,
  ?'baz' => int,
  ...
);

function get_value(): MyShape{
  return shape(
    'foo' => 123,
    'bar' => 456,
    /* Typo of 'baz', but there's no type error: the typechecker can't know if
     * this is a typo of an optional field, or if 'baz' was intentionally
     * omitted and 'baa' is meant to be an additional field. */
    'baa' => 789,
  );
}

Historical Note

Prior to HHVM 3.23, all shapes allowed structural subtyping; this was changed because of the hard-to-debug issues mentioned above.