概要
Result<int, RuntimeException> を instanceof Ok / instanceof Err で絞り込むと、PHPStan は型引数を失った Ok / Err(型引数なし)に絞り込み、unwrap() / unwrapErr() が mixed になる。
isOk() / isErr()(@phpstan-assert-if-true Ok<T> $this)経由なら Ok<int> / Err<RuntimeException> が保たれる。
再現
/** @param Result<int, RuntimeException> $result */
function f(Result $result): void
{
if ($result instanceof Ok) {
// actual: Valbeat\Result\Ok(型引数なし)、unwrap() は mixed
}
if ($result->isOk()) {
// actual: Valbeat\Result\Ok<int>、unwrap() は int
}
}
Ok<T> implements Result<T, never>・@template-covariant・@phpstan-sealed は宣言済みであり、Result<int, E> のサブタイプである Ok は論理的に Ok<int> しかあり得ないため、Ok<int> への絞り込みは健全。PHPStan が instanceof で逆方向のテンプレート解決をしないことが原因(PHPStan 2.2.2 で実測)。
対応状況
アップストリーム関連 issue
概要
Result<int, RuntimeException>をinstanceof Ok/instanceof Errで絞り込むと、PHPStan は型引数を失ったOk/Err(型引数なし)に絞り込み、unwrap()/unwrapErr()がmixedになる。isOk()/isErr()(@phpstan-assert-if-true Ok<T> $this)経由ならOk<int>/Err<RuntimeException>が保たれる。再現
Ok<T> implements Result<T, never>・@template-covariant・@phpstan-sealedは宣言済みであり、Result<int, E>のサブタイプであるOkは論理的にOk<int>しかあり得ないため、Ok<int>への絞り込みは健全。PHPStan が instanceof で逆方向のテンプレート解決をしないことが原因(PHPStan 2.2.2 で実測)。対応状況
testInstanceofOkNarrowing/testMatchArmNarrowing)— PHPStan 側が改善されたらテストが落ちて検知できるisOk()/isErr()を推奨(docs: document instanceof narrowing limitation, recommend isOk()/isErr() #82)Ok<int>/intに更新し、README / PHPDoc の注記を削除アップストリーム関連 issue
instanceofis used. phpstan/phpstan#13204 — Generic type is not inferred at all when instanceof is used(closed / 2026-03 修正済み)。テンプレート境界からの推論の話で、本ケース(既知の型引数の実装クラスへの射影)は未解決