never型で「この先は実行されない」を型として伝える
PHPのnever型とは、その関数が値を返さず必ず例外を投げるか処理を終了することを示す戻り値型です。PHP 8.1で追加され、呼び出した直後のコードが実行されないことを言語レベルで保証します。
例外を投げるだけの小さなヘルパーを書いたことはないでしょうか。fail('見つかりません') のような、投げるためだけの関数です。私はよく書きます。ただ、戻り値型に何を書けばいいのか毎回少し迷っていました。void と書くと嘘になるし、かといって書かないのも気持ち悪い。
その迷いに答えを出すのが never です。地味な型ですが、使いどころを知っておくと読み手にも静的解析にも効きます。
なぜvoidではダメなのか
void は「値を返さない」型です。ただしそれは「返り値がない」だけで、関数は普通に終わって、呼び出し元の次の行に処理が戻ってきます。
function sayHello(string $name): void {
echo "Hello {$name}";
}
sayHello('World');
echo ', nice to meet you'; // ここは実行される
一方 never は「そもそも戻ってこない」型です。呼び出した時点で、そのあとの行は実行されないことが確定します。
function redirect(string $uri): never {
header('Location: ' . $uri);
exit;
}
redirect('/login');
echo 'ここは絶対に実行されない';
読み手にとっての違いは「この関数のあとを読む必要があるか」です。void なら続きを読む、never なら読まなくていい。たった1語で制御フローの説明になっているのが、この型の一番おいしいところだと思います。
never型はどこに書ける?
void と同じで、書けるのは戻り値型の位置だけです。引数やプロパティの型に書くとコンパイルエラーになります。
class A {
public never $x; // Fatal error
public function f(never $v) {} // Fatal error
}
もうひとつ、never はユニオン型の一部にできません。int|never のような書き方は通らない、と覚えておけば十分です。型理論でいうところの一番下(ボトム型)なので、他の型と足し合わせても意味がない、という理屈ですね。
ジェネレータにもできません。関数の中に yield があって戻り値型が never だと、これもコンパイル時に弾かれます。
うっかり return を書くとどうなるか
ここが実務で一番効くポイントです。never と宣言した関数に return を書くと、PHPが止めてくれます。
function redirect(string $uri): never {
if ($uri === '') {
return; // Fatal error: A never-returning function must not return
}
header('Location: ' . $uri);
exit;
}
値なしの return; でもアウトです。これはコンパイル時に検出されます。
やっかいなのは、return を書いていないのに関数の末尾まで到達してしまうケースです。こちらは書いた時点では通ってしまい、実際に到達したときに TypeError が飛びます。
function redirect(string $uri): never {
if ($uri !== '') {
header('Location: ' . $uri);
exit;
}
// 空文字のときは、ここまで来て暗黙のreturnになる
}
redirect(''); // TypeError: never-returning function must not implicitly return
あとから条件分岐を足したときに踏みやすい罠です。never な関数を書いたら、あらゆる経路が throw か exit か無限ループで終わっているかを、分岐を足すたびに確認する癖をつけたほうがいいです。逆に言えば、確認を忘れても本番で TypeError として顕在化するので、黙って変な値が流れるよりはマシ、とも言えます。
継承では戻り値をneverに狭められる
never はすべての型の部分型なので、親の戻り値型が何であっても、子クラスで never に狭めることができます。
abstract class Person
{
abstract public function hasAgreedToTerms(): bool;
}
class Kid extends Person
{
public function hasAgreedToTerms(): never
{
throw new LogicException('子どもは規約に同意できません');
}
}
「このサブクラスではこの操作自体が成立しない」を型で表明できるわけです。__toString() のような、シグネチャが決まっているメソッドでも同じことができます。
逆向きはできません。親が never を返すと宣言しているのに、子が void を返すように書くと、変性違反でFatal errorになります。親が「ここから先はない」と約束している以上、子がそれを破れないのは筋が通っていますね。
ちなみに、常に例外を投げる関数に never 以外を書くのは自由です。function doFoo(): int { throw new Exception(); } は問題なく通ります。never はあくまで「より正確に書ける選択肢」であって、義務ではありません。
実務での使いどころ
一番素直なのは、例外を投げるだけのヘルパーです。never を付けておくと、null合体演算子と組み合わせたときに読みやすくなります。
function fail(string $message): never {
throw new DomainException($message);
}
$user = $repo->find($id) ?? fail("user {$id} が見つかりません");
// この行を抜けた時点で $user は必ずUser
嬉しいのは、PHPStanやPsalmといった静的解析ツールがこの情報を理解してくれることです。fail() の先は到達しないと分かるので、$user がnullかもしれないという警告が消えます。docblockに @return noreturn と書いて解析ツールに教えていた時代と比べると、ずいぶん素直になりました。
もうひとつは、リダイレクトや致命エラー時の終了処理です。exit を内側に隠した関数は、外から見ると普通の関数と区別がつきません。never が付いていれば、「この呼び出しでリクエストが終わる」がシグネチャだけで伝わります。
逆に、付けないほうがいい場面もあります。「今のところ必ず例外を投げるが、将来は値を返すかもしれない」関数です。never は継承でもAPIとしても強い約束なので、あとから緩める側に変更しづらい。確定した終端にだけ使う、くらいの温度感がちょうどいい気がしています。
まとめ
void は「戻ってくるが値はない」、never は「戻ってこない」。この一行の違いを型に落とせるようになったのがPHP 8.1でした。書ける場所は戻り値型だけで、ユニオン型には入れられず、うっかり return すると怒られる。制約は多いですが、その制約こそが読み手への保証になっています。例外を投げるだけの小さなヘルパーを見つけたら、まずそこに一語足してみるのがいいと思います。
よくある質問
Q. neverとvoidはどちらを使えばいいですか?
A. 呼び出し元に処理が戻るなら void、必ず例外か exit で終わるなら never です。判断に迷うということは戻る経路がある可能性が高いので、その場合は void のほうが安全です。
Q. never型はPHPのどのバージョンから使えますか?
A. PHP 8.1からです。8.0以前では never という名前のクラスを指す型とみなされてしまい、意図した動作になりません。古い環境も対象にするライブラリでは、docblock の @return never で静的解析ツールに伝える形になります。
Q. never型の関数が最後まで到達したらどうなりますか?
A. 実行時に TypeError が投げられます。明示的な return はコンパイル時のFatal errorですが、暗黙のreturnは実行されて初めて分かるので、分岐を追加したときは全経路が終端しているか確認してください。