Typechecker: Modes

When you are writing Hack code, you will typically start your file with <?hh and start writing code. However, that one top line is actually quite important in how the typechecker interprets your code.

Partial Mode

Code in Hack files starting with the four characters <?hh at the top of it is said to be in partial mode. This means that the typechecker will check whatever it can, but no more; it does not insist on full type coverage. Partial mode is great to use to start gradually typing existing code. Here are the rules for partial mode:

  • You can fully interoperate with PHP files -- you can call functions and use classes that the typechecker cannot see. This code is assumed to exist in a <?php file somewhere. See the discussion of the assume_php configuration option for a discussion of the implications of this, why it might not be desirable, and how to change it.
  • You can write code at the very top-level (i.e., outside functions and methods), but it is not typechecked. To maximize the typechecker's ability to check code in partial mode, it is common practice to wrap all top-level code into one main function and have a call to that function be the only code at top-level.
  • You can use references &, but the typechecker doesn't attempt to check them (for the purposes of typechecking, it ignores the &). Try not to use references since you can easily use them to break the type system.
  • You can access superglobals without error.
<?hh

namespace Hack\UserDocumentation\TypeChecker\Modes\Examples\Partial;

use \Hack\UserDocumentation\TypeChecker\Modes\Examples\NonHack as NonHack;

// This function isn't type annotated, so callers will be able to do whatever
// they want with its result. However, the typechecker does still look at and
// check the body of the function.
function foo() {
  $a = 1;
  // This will geneate a type error:
  //   an int does not allow array append (Typing[4006])
  $a[] = 2;
}

class A {
  private int $x;

  public function __construct() {
    $this->x = 9;
  }

  public function getX(int $y): ?int {
    return $y > 4 ? $this->x : null;
  }

  // You can even have non-type annotated code in the same class as
  // type annotated code.
  public function notTyped($z) {
    return "Hello" . $z;
  }
}

function bar(): int {
  $a = new A();

  // Not typechecked either. So we can pass an int and it will be converted to
  // a string by the runtime, luckily.
  echo $a->notTyped(3);

  // The return value from this call is not typechecked since B is in a PHP
  // file -- the typechecker assumes we know what we are doing since the
  // annotation is missing.
  $b = NonHack\B::getSomeInt();
  echo NonHack\php_func(3, $b);

  $i = $a->getX($b);
  if ($i !== null) {
    return $i;
  } else {
    return 0;
  }
}

bar();
Output
string(4) "1004"
Hello33100

Notice that we have annotated some of the code, but not all of it. Inferred code is checked regardless of annotations on the function itself.

Strict Mode

Hack files starting with:

<?hh // strict

means that the typechecker enforces strict typing rules in that file. If at all possible, start new projects with strict mode -- if every file in a codebase is in strict mode, the typechecker's coverage will be maximized since it will be able to fully check everything, and there can be no type errors at runtime.

Here are the rules for strict mode:

  • All functions and methods must be fully annotated, i.e., their parameters and return types must be fully specified. Any location that can have a type annotation must have one.
  • All functions called and classes referenced must be defined in a Hack file. Thus, for example, if your strict mode code tries to use a class defined in a <?php file, you will get an error because the typechecker doesn't look in <?php files and won't know about that class. However, from strict mode, you can call into Hack code in partial and decl mode.
  • The only code allowed at the top-level are require, require_once, include, include_once, namespace, use, and definitions of classes, functions, enums, and constants.
  • No references & anywhere.
  • No accessing superglobals.

Strict is the mode you want to strive to. The entire typechecker goodness is at your disposal and should ensure zero runtime type errors.

<?hh // strict

namespace Hack\UserDocumentation\TypeChecker\Modes\Examples\Strict;

use \Hack\UserDocumentation\TypeChecker\Modes\Examples\NonHack as NonHack;

function foo(): void {
  $a = 1;
  // This will generate a type error:
  //   an int does not allow array append (Typing[4006])
  $a[] = 2;
}

class A {
  private int $x;

  public function __construct() {
    $this->x = 9;
  }

  public function getX(int $y): ?int {
    return $y > 4 ? $this->x : null;
  }

  // In partial, this didn't have to be annotated. In strict, it does.
  public function notTyped(string $z): string {
    return "Hello" . $z;
  }
}

function bar(): int {
  $a = new A();
  // This is typechecked, so we can't pass an string-y int; we must pass a
  // string
  echo $a->notTyped("3");

  // Cannot call these in strict mode:
  //    Unbound name:
  //       Hack\UserDocumentation\TypeChecker\Modes\Examples\NonHack\B
  //       (an object type) (Naming[2049])
  $b = NonHack\B::getSomeInt();
  //    Unbound name:
  //       Hack\UserDocumentation\TypeChecker\Modes\Examples\NonHack\php_func
  //       Typing[4107])
  echo NonHack\php_func(3, $b);

  $i = $a->getX(100);
  if ($i !== null) {
    return $i;
  } else {
    return 0;
  }
}

// This can't be in strict mode either. You need to put this in partial file
// and include it from this file. For the purposes of this example, though,
// we'll just suppress the error.

/* HH_FIXME[1002] So we can get interesting type-checking errors */
bar();
Output
string(4) "1004"
Hello33100

Notice that we cannot call into the <?php file any longer and that all entities in the Hack file are annotated.

Decl Mode

Hack code starting with

<?hh // decl

is in decl mode. Decl mode code is not typechecked. However, the signatures of functions, classes, etc in decl mode are extracted, and are used by the typechecker when checking other code. Decl mode is most useful when converting over code written in PHP: although the body of that code might have type errors that you don't want to deal with right away, it's still beneficial to make the signature of that code, or at the very least its mere existence, visible to other code. In fact, one very basic migration path to Hack is to just change all your <?php files to decl mode, then start taking each file one-by-one and making them partial.

New Hack code should never be written in decl mode.

<?hh // decl

// Before this was <?php code. Now the typechecker can see the signatures of
// these functions and classes for when Hack calls them, even in strict mode.

namespace Hack\UserDocumentation\TypeChecker\Modes\Examples\Decl;

function php_func($x, $y) {
  return $x . $y;
}

class B {
  static function getSomeInt() {
    return 100;
  }
}
<?hh // strict

namespace Hack\UserDocumentation\TypeChecker\Modes\Examples\CallIntoDecl;

require __DIR__ . '/decl.inc.php';
// This actually makes the call to calling_into_decl() since we cannot have
// top level functions in strict mode
use \Hack\UserDocumentation\TypeChecker\Modes\Examples\Decl as Decl;

function calling_into_decl(): string {
  // If php_func wasn't in decl mode, then we would get an unbound name error.
  // As it is, we can call this function and the typechecker will ensure we are
  // passing in the right number of arguments, but not the types of them.
  return Decl\php_func("a", "b");
}
<?hh

namespace Hack\UserDocumentation\TypeChecker\Modes\Examples\CallIntoDecl;

var_dump(calling_into_decl());

Output

string(2) "ab"

The example shows all three modes. First it shows a decl mode file that used to be in <?php. Nothing else was added except the header change. Then it shows a strict mode file calling into the decl mode file. The typechecker knows the signatures of the functions and classes and can ensure basic things like whether you are calling a named entity and passing the right number of arguments. Finally, we have a partial mode file that actually calls the function in strict mode because we cannot have top-level function calls in strict mode.

Mixing Modes

As shown in the example above, modes can be freely mixed; each file in your project can be in a different typechecker mode.