ユニオン型を使うときは、共変性と反変性という考え方を意識しておく必要があります。詳しくはドキュメントで説明されていますが、以下の例も参考になるはずです。
先ほどの例と同じような関数を使うCalculatorクラスがあるとします。
class Calculator
{
public function add(int|float $x, int|float $y): int|float
{
return $x + $y;
}
}
このクラスを継承する場合、仮引数の型と戻り値の型を変更できますが、それは次のルールに従う場合に限られます。
まず、仮引数の型はより「広い」ものにできます。つまり、基底クラスよりも多くの型を受け付けられるようになるということです。たとえば、次のような書き方は許されます。
class StringCalc extends Foo
{
public function add(float|int|string $x, float|int|string $y): int|float
{
if (!is_numeric($x) || !is_numeric($y)) {
throw new \InvalidArgumentException('$x and $y must be numeric');
}
return $x + $y;
}
}
StringCalcを元のCalculatorの代わりに使っても、すべて問題なく動作します。呼び出し側は元と同じようにfloatやintを渡せますし、戻り値の型も期待どおりのintかfloatのままです。
同様に、戻り値の型を狭めることもできます。ユニオン型から型を減らして、より具体的にするということです。次の例はこのルールを利用しています。
class IntCalc extends Calculator
{
public function add(float|int $x, float|int $y) : int
{
return (int)($x + $y);
}
}
IntCalcはintとfloatを引き続き受け付けるので、元のCalculatorの代わりに問題なく使えます。
何が許されて何が許されないかについて詳しく知りたい場合は、リスコフの置換原則(LSP)を参照してください。
許されない例については、次のInvalidCalcを見てください。
class InvalidCalc extends Calculator
{
public function add(int $x, int $y) : int|float|string
{
// Declaration must be compatible with Calculator->add(x: float|int, y: float|int)
// Return type declaration must be compatible with Calculator->add(x: float|int, y: float|int) : float|int
}
}
InvalidCalcクラスはコンパイルできません。問題は2つあります。受け付けられる仮引数の型を狭めてしまったため、InvalidCalcはfloatを受け付けられなくなり、元のCalculatorの代わりにはなれません。さらに、stringを返せるようになっていますが、これも元のクラスで許されている戻り値の型には含まれていません。