こんにちは!HackとHHVMの世界へようこそ。
レガシーなPHPコードベースをHackへと移行し、強力な型安全性とHHVMの圧倒的な実行パフォーマンスを手に入れようとする際、誰もが最初にぶつかる大きな壁があります。それが「`mixed`型の氾濫」です。
型がよく分からないからといって関数の引数や戻り値に安易に`mixed`をつけてしまうと、Hackの型チェッカー(`hh_client`)はその先で厳格なエラーを吐き出し始めます。「動的型付けの気軽さ」を引きずったままでは、HackのStrict Mode(厳格モード)の世界へ足を踏み入れることはできません。
この記事では、PHPからHackに入ってきた方や型システム初心者のエンジニアに向けて、「なぜ`mixed`が問題になるのか」という本質から、`mixed`を段階的に排除して完全なStrict Modeへ引き上げる実践的なリファクタリング・ロードマップを分かりやすく解説します。
ここをクリアすれば、Hackの型システムの基本はバッチリマスターできますよ!一緒にステップバイステップで進めていきましょう。
—
1. そもそもHackの「mixed」型とは何者なのか?
TypeScriptや他言語の経験がある方は、`mixed`を「何でも代入できる型(`any`のようなもの)」と思っていませんか?実は、Hackにおける`mixed`はTypeScriptの`any`ではなく、`unknown`に近い概念です。
【TypeScriptの any】 何でも入る + 何でも呼べる(型チェックを無力化する危険な穴)
【Hackの mixed】 何でも入る + そのままでは何も呼べない(最上位の安全な型)
Hackの型階層において、`mixed`はすべての型の頂点(Top Type)に君臨しています。
┌───────────────┐
│ mixed │ (すべての親)
└───────┬───────┘
┌─────────────┼─────────────┐
▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐
│ int │ │string │ │MyClass│ …
└───────┘ └───────┘ └───────┘
つまり、「どんな値でも受け取れる」のですが、型チェッカー視点では「中身が何だか特定できていない状態」です。そのため、`mixed`型の値に対して直接メソッドを呼んだり、プロパティにアクセスしたり、計算を行おうとすると、型チェッカーが即座にエラーを出します。
レガシーコードがStrict Modeで怒られる典型例
// ❌ Strict Modeでは即座にエラーになります
function processUserData(mixed $data): void {
// Typing[4064] You are trying to access a field on an object of type mixed
echo $data[‘name’];
// Typing[4062] You are trying to access a method on a value of type mixed
$data->save();
}
「何でも入る箱」として`mixed`を宣言したのに、中身を触ろうとした瞬間にコンパイラに止められてしまう……これが、初心者が最初に戸惑うポイントです。
—
2. 段階的リファクタリング・ロードマップ(4つのステップ)
では、レガシーなPHPの「何が入ってくるかわからない動的コード」を、どのようにして型安全な状態へ昇華させればよいのでしょうか?
一気に書き換える必要はありません。以下の4つのステップで、段階的に安全性を高めていきましょう。
[Step 1] 型ガード(Refinement)で mixed の中身を絞り込む
│
[Step 2] レガシー配列(array)を dict / vec / shape に構造化する
│
[Step 3] Nullable(?T)とジェネリクス(
│
[Step 4] 完全なStrict Modeへ引き上げ、mixed を根絶する!
—
Step 1: 型ガード(Type Refinement)で局所的に型を絞り込む
まずは、引数の`mixed`をいきなり消せない場合の対処法です。
Hackでは、条件分岐の中で`is`演算子や標準のチェック関数を通すことで、型チェッカーが自動的にそのスコープ内の型を「絞り込み(Refine)」してくれます。
function formatId(mixed $id): string {
// 1. is演算子を使って型をテストする
if ($id is int) {
// このブロック内では、$id は確実に `int` として扱われます
return “ID_”.Str\format(“%05d”, $id);
}
if ($id is string) {
// このブロック内では、$id は確実に `string` です
return “ID_”.Str\trim($id);
}
// 想定外の型が来たら例外を投げて早期脱出(Fail Fast)
throw new InvalidArgumentException(“IDはintまたはstringである必要があります”);
}
> 先輩のアドバイス:
> 「何が来るか分からない」なら、コードの入り口でガードを立てて型を保証するのが基本です。これで関数の内部処理は完全に安全になります。
—
Step 2: レガシー配列を「shape」「dict」「vec」へ置き換える
レガシーPHPで最も`mixed`を生み出す元凶が「連想配列(連想・インデックス混在のarray)」です。
Hackでは、用途に合わせてコレクションを明確に区別します。
- `vec
` : 順序付きの配列(0, 1, 2…の数値インデックス) - `dict
` : 純粋なキー・バリューマップ(キーはintまたはstring) - `shape(…)`: 特定のキーと型の構造を持つ軽量レコード(最も強力!)
特にAPIのレスポンスやDBレコードのような「決まったキーを持つ連想配列」は、`shape`を定義することで劇的に安全になります。
❌ Before: 何が入っているか不明な連想配列
// 何のキーがあるのか、値の型が何なのかチェッカーには一切分かりません
function updateUser(dict
$name = (string)$user[‘name’]; // 危険なキャストが必要
}
⭕ After: `shape` 型で厳密に構造を定義
// ユーザー構造を明示的に型定義する
type UserPayload = shape(
‘id’ => int,
‘name’ => string,
?’email’ => string, // ?をつけると省略可能なキー(Optional field)になる
);
function updateUser(UserPayload $user): void {
// 型安全! $user[‘name’] は100% string であることが保証される
echo “Updating: “.$user[‘name’];
// 省略可能フィールドは Shapes::idx を使うと安全
$email = Shapes::idx($user, ‘email’, ‘no-email@example.com’);
}
—
Step 3: Nullable型とジェネリクスを活用する
「値が存在しないかもしれない」「様々な型を扱いたいけれど`mixed`にはしたくない」という場面では、Nullable型(`?T`)とジェネリクス(`
Nullable型(`?T`)
`null`を許容したい場合は型の前に `?` をつけます。
// ?User は「Userのインスタンス」または「null」を意味します
function findUser(int $id): ?User {
if ($id <= 0) {
return null;
}
return new User($id);
}
ジェネリクス(型引数)
コンテナ的な処理で「中身の型は何でも良いが、入れたものと同じ型を取り出したい」という場合は、ジェネリクスを使います。
// ❌ mixed を使うと、取り出した時に型情報が消えてしまう
function getFirstMixed(vec
return $items[0] ?? null;
}
// ⭕ ジェネリクス
function getFirst
if (C\is_empty($items)) {
return null;
}
return $items[0];
}
// 呼び出し側:
// $first は自動的に string 型として推論されます!(ダウンキャスト不要)
$first = getFirst(vec[‘apple’, ‘banana’, ‘orange’]);
—
Step 4: 関数のシグネチャから `mixed` を一掃する
局所的な型ガード、`shape`の導入、ジェネリクスの適用が終われば、関数のシグネチャ(引数と戻り値)から`mixed`を完全に排除できるようになります。
// ✨ 完成形:完全に型が固定された堅牢なHackコード
final class UserRegistrationService {
public function register(
UserPayload $payload,
vec
): RegistrationResult {
// 型チェッカーが最初から最後まで完全に矛盾がないかを検証してくれます
$userId = $this->dbInsert($payload[‘name’]);
return new RegistrationResult($userId, true);
}
private function dbInsert(string $name): int {
// DB登録処理…
return 42;
}
}
ここまで来れば、ファイル先頭の型チェックは完璧にパスします。HHVMは実行時に無駄な型チェックや暗黙の型変換を行う必要がなくなり、最高速のJITコンパイル性能を発揮します。
—
3. 現場でありがちな落とし穴と対処法
⚠️ 落とし穴1: 強制キャスト `(int)$val` に頼りすぎる
PHPの手癖で `(int)$mixedVal` や `(string)$mixedVal` とキャストしたくなりますが、Hackではオブジェクトや不正な配列をキャストしようとすると実行時エラーになります。
キャストではなく、`is` 演算子での分岐や、`Str\to_int()` などの安全な標準ライブラリ関数を使いましょう。
⚠️ 落とし穴2: JSONのデコード結果
`json_decode` の結果は、本質的に「外から来る不確定なデータ」なので `mixed` になります。
Hackでは、JSONデコード直後に型安全なパーサーライブラリ(例: `HHVM\Lib\Experimental\Json` や型アサーションライブラリ)を使って、速やかに特定の `shape` やオブジェクトに変換するのがベストプラクティスです。
—
まとめ
レガシーPHPからHackへの移行は、「何でもありの自由」から「コンパイラに守られた秩序」へとシフトする旅です。
1. `mixed` はTypeScriptの `any` ではなく `unknown` であると理解する
2. `mixed` が必要な境界線では、`is` 演算子の型ガードで即座に絞り込む
3. 曖昧な連想配列は `shape` や `dict` / `vec` に置き換える
4. 抽象的な処理には ジェネリクス(`
このロードマップに沿ってリファクタリングを進めていけば、大量のレガシーコードも確実に、安全にStrict Modeへと引き上げることができます。
型チェッカーのエラーが「ゼロ」になり、`hh_client` が `No errors!` を返した瞬間の快感は格別ですよ。ぜひ現場のコードで試してみてくださいね!