From PHP to Hack: Slack’s Incremental Path to Static Types

Slack launched in 2014 on PHP 5, and like several other large operations, moved to HHVM in 2016 to run that code faster. What kept Slack on HHVM was not just speed, but the new language it enabled: Hack (often searchable as Hacklang). Hack grew out of PHP as a superset, but it adds a static type checker and a more expressive type system. The result has changed how Slack’s backend developers work, making refactoring safer and catching a whole class of bugs before code ever runs.

Checking Types Before Runtime

PHP’s type system has improved substantially since the PHP 5 era, when return types, class properties, and scalar types couldn’t be annotated. Yet its fundamental gap persists: types are only validated at runtime, which is the most expensive moment to discover a type error—whether through a broken test suite, a production error log, or a user report.

Hack moves type checking to design time. The static checker runs without executing code, and integrated tooling surfaces errors as you type. Change a function signature with hundreds of call sites, and the places needing updates light up before you save. This is the difference between catching a bug milliseconds after typing versus waiting for test results or a deployment. Slack developers find they don’t bother running code until the type checker passes—and by then, it usually just works. That frees testing effort for logic bugs, which static typing cannot catch.

Community packages like Psalm and PHPStan can bring static checking to PHP projects, and Slack recommends them for PHP users. But Hack’s checker works with a richer type system designed for static analysis from the ground up, including generics, shapes, enums, hack arrays, and a well-typed standard library.

Incremental Migration with Gradual Typing

Slack started in Hack’s partial mode, which treats untyped values as the “any” type, usable for any purpose—the same approach TypeScript takes for migrating JavaScript. This enabled a gradual, file-by-file migration to typed code. As files became fully typed, Slack switched them to strict mode to keep them that way.

Working this way changed how engineers thought about type safety. Adding types was not a compiler gate but a deliberate choice to add value. Some parts of the codebase were easy to type; others required refactoring first. The payoff went beyond bug prevention: types act as verifiable inline documentation (unlike comment blocks) and as contracts between parts of the codebase, which matters in a large shared backend like Slack’s.

Shapes for Complex Structures

PHP’s single array type confusingly serves as both a list (ordered values) and a map (key-value pairs). Functions like array_merge treat these two uses differently, which is a chronic source of bugs. Hack separates these into distinct types—vec<string> for a list of strings, dict<string, int> for a map with string keys and integer values.

But what about a dict that holds several different types of values? dict<string, mixed> is technically valid but not very useful. Hack’s shapes solve this: a shape is an array with known keys and specific types, with optional keys marked by ?. For example, a shape can represent the arguments of an HTTP POST request, where many fields are optional. A function signature can then type $options using that shape.

type http_post_options = shape(
  ?'timeout' => int,
  ?'port' => int,
  ?'http_basic_auth' => string,
  ?'headers' => dict<string, string>,
  ?'form_data' => dict<string, string>,
  ?'json_payload' => JsonSerializable,
  ?'user_agent' => string,
  ?'follow_redirects' => bool,
);
function http_post(
  string $url,
  http_post_options $options
): http_response {
  // ... implementation here
}

A call site might look like this:

$result = http_post('https://example.com', shape(
  'timeout' => 10,
  'form_data' => <em>dict</em>['example' => 'test'],
));

Shapes not only enforce correct types per field; they catch typos in key names, both at the call site and in the function body. Before shapes, understanding such a function’s arguments meant reading its body or a possibly outdated doc block. At Slack, shapes are widely used for database rows (with code-generated shapes from the DB schema), expected results of decoding JSON payloads, and functions with many optional arguments.

Async Without the Threading Headaches

As Slack’s feature set grows, each request does more work. Keeping the experience fast requires concurrency—doing multiple tasks at once within a single request. In many languages, that means mutexes, thread-safe data structures, or callbacks, all of which complicate reasoning and debugging.

Hack supports the async/await pattern for multitasking without multithreading. Functions can pause while waiting for I/O, letting the runtime schedule other work. Migrating code to concurrency often only means adding the async and await keywords and following a few guidelines, preserving the code’s mental model. Slack uses the concurrent block to fetch data from multiple sources at once, where the fetches previously ran sequentially:

async function get_mentions(User $user): Awaitable<vec<Mention>> {
  concurrent {
    // fetch @user mentions
    $at_mentions = await get_at_mentions($user);
    // fetch @channel mentions for channels the user is in
    $channel_mentions = await get_at_channel_mentions($user); 
  }
  return sort_mentions($at_mentions, $channel_mentions);
}

Leaving PHP Behind

HHVM’s decision to break compatibility with PHP was controversial, and Slack had to purge every last line of PHP and its dependencies from the codebase. Since the HHVM 4.0 release removed PHP support, however, the language has evolved rapidly. The developers have stripped out “PHPisms” that hindered type safety and performance, while adding features like reified generics, type assertions, and the using statement. Keeping pace with these updates across a large codebase is nearly a full-time job.

The main drawback is losing access to the extensive PHP ecosystem on Packagist. Hack projects can still be published there, and several high-quality libraries exist:

  • HHAST enables expressive lint rules and automated code migrations via a syntax tree, unlike PHP tooling that parses a token stream.
  • Hack JSON Schema, open-sourced by Slack, uses Hack Codegen to generate Hack code and type definitions from JSON schema definitions.
  • Hack SQL Fake simulates MySQL for unit tests, handling millions of SQL queries in every Slack test run.
  • XHP provides type-safe, async server-side rendered HTML and shares history with React’s JSX.

As Hack distances itself from its PHP origins, it is becoming a language in its own right. While gradual migration from PHP is no longer feasible, developers familiar with PHP may turn to Hack for new projects. The broader industry trend—adding static type checking to interpreted languages, as seen with Python, JavaScript, and Ruby options—suggests that combining interpreter convenience with compile-time checking is worth considering for codebases of any size.