Quando usas tipos de união, tens de conhecer os conceitos de covariância e contravariância. Estão explicados na documentação, mas estes exemplos podem ajudar.
Suponhamos que temos uma classe Calculator que usa uma função como no exemplo acima:
class Calculator
{
public function add(int|float $x, int|float $y): int|float
{
return $x + $y;
}
}
Se estenderes a classe, podes alterar os tipos dos parâmetros e os tipos devolvidos, mas apenas de acordo com as seguintes regras.
Primeiro, os tipos dos parâmetros podem ser "mais amplos", ou seja, podem aceitar mais tipos do que a classe base. Por exemplo, isto é permitido:
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;
}
}
Podíamos substituir o Calculator original por StringCalc e tudo continuaria a funcionar. Quem chama continuaria a poder enviar o float ou int que já podia enviar, e o tipo devolvido continuaria a ser o int ou float esperado.
Do mesmo modo, podes estreitar o tipo devolvido, ou seja, torná-lo mais específico removendo tipos da união. Este exemplo tira partido dessa regra:
class IntCalc extends Calculator
{
public function add(float|int $x, float|int $y) : int
{
return (int)($x + $y);
}
}
Como IntCalc continua a aceitar int e float, pode continuar a substituir um Calculator original sem problemas.
Para saberes mais sobre o que é e o que não é permitido, podes consultar o Princípio de Substituição de Liskov (LSP).
Para veres um exemplo do que não seria permitido, dá uma vista de olhos ao InvalidCalc abaixo:
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
}
}
A classe InvalidCalc não compila e tem dois problemas. Ao reduzir o que é aceite como parâmetro, InvalidCalc não poderia substituir o Calculator original, porque já não aceita float. Além disso, agora poderia devolver uma string, o que também não está dentro dos tipos devolvidos permitidos pela classe original.