Tip
Use overloads for type-safe polymorphic APIs where the return type depends on the input type.
Tier 1 · Story 11Complete
Tyhp supports function and method overloading, allowing you to declare multiple signatures for the same function or method with different parameter types and specialized return types. Overload signatures are compile-time declarations that describe how the function behaves for specific input types. The final declaration must include a body (the implementation) that covers all overloaded variants. At compile time, the compiler selects the most specific overload based on argument types. In the PHP output, all overload signatures are stripped and only the single implementation remains — overloading does NOT create multiple PHP functions.
Overload signatures are declarations that end with a semicolon (no body). The final declaration of the function must have a body that handles all possible parameter combinations. The compiler uses the overload signatures to determine the precise return type at each call site.
<?tyhp
// Overload: when $convertToInt is true, returns int
function convertNumber(string|int|float $value, true $convertToInt): int;
// Overload: when $convertToInt is false, returns float
function convertNumber(string|int|float $value, false $convertToInt): float;
// Implementation: covers all scenarios
function convertNumber(string|int|float $value, bool $convertToInt = false): int|float
{
return $convertToInt ? \intval($value) : \floatval($value);
}
// The compiler knows the precise return type at each call site:
int $asInt = convertNumber("42", true); // Compiler knows: returns int
float $asFloat = convertNumber("42", false); // Compiler knows: returns float
int|float $maybe = convertNumber("42"); // Uses default: returns float
Overload signatures are completely stripped from the PHP output. Only the final implementation with the body is emitted. This means overloading is purely a compile-time feature with zero runtime cost.
<?php
function convertNumber(string|int|float $value, bool $convertToInt = false): int|float
{
return $convertToInt ? \intval($value) : \floatval($value);
}
Overloads are especially powerful when combined with static value types. A static value type constrains a parameter to a specific literal value (true, false, 0, 1, a specific string, etc.). This allows the compiler to narrow the return type based on the exact value passed.
<?tyhp
function parseValue(string $input, 'json' $format): array;
function parseValue(string $input, 'csv' $format): array<string>;
function parseValue(string $input, 'raw' $format): string;
function parseValue(string $input, string $format): array|string
{
return match($format) {
'json' => \json_decode($input, true),
'csv' => \str_getcsv($input),
default => $input,
};
}
// Compiler knows exact return types:
array $json = parseValue($data, 'json');
string $raw = parseValue($data, 'raw');
<?php
function parseValue(string $input, string $format): array|string
{
return match($format) {
'json' => \json_decode($input, true),
'csv' => \str_getcsv($input),
default => $input,
};
}
Method overloads work identically to function overloads but are declared inside a class. All overload signatures for a method must have the same modifiers (visibility, static, final, etc.).
<?tyhp
class Repository
{
// Overload: find by ID returns exact type
public function find(int $id): User;
// Overload: find by email might not exist
public function find(string $email): ?User;
// Implementation with body
public function find(int|string $criteria): ?User
{
if (\is_int($criteria)) {
return $this->findById($criteria);
}
return $this->findByEmail($criteria);
}
}
<?php
class Repository
{
public function find(int|string $criteria): ?User
{
if (\is_int($criteria)) {
return $this->findById($criteria);
}
return $this->findByEmail($criteria);
}
}
Async methods can also have overloads. All overload signatures and the implementation must be marked with async. The compiler ensures type-safe resolution for both sync and async overloads.
<?tyhp
class ApiClient
{
async public function fetch(int $id): Response;
async public function fetch(string $url): Response;
async public function fetch(int|string $target): Response
{
$url = \is_int($target) ? "/api/items/{$target}" : $target;
return await $this->httpClient->get($url);
}
}
<?php
class ApiClient
{
public function fetch(int|string $target): \Tyhp\Promise
{
return \Tyhp\Promise::_async(function () use ($target) {
$url = \is_int($target) ? "/api/items/{$target}" : $target;
return \Tyhp\Promise::_await($this->httpClient->get($url));
});
}
}
The final implementation can use the short fn syntax if the body is a single expression. The overload signatures remain semicolon-terminated as usual.
<?tyhp
function negate(int $value): int;
function negate(float $value): float;
fn negate(int|float $value): int|float => -$value;
<?php
function negate(int|float $value): int|float
{
return -$value;
}
The compiler selects the most specific matching overload at compile time using these rules:
Use overloads for type-safe polymorphic APIs where the return type depends on the input type.
Combine overloads with static value types for maximum type narrowing — the compiler can determine exact return types at each call site.
Keep overload signatures compatible with the implementation — the implementation must be able to handle every call described by the overloads.
Keep method modifiers consistent across all overloads and the implementation.
<?tyhp
// DO: ensure the implementation covers all overloads
function stringify(int $value): string;
function stringify(float $value): string;
function stringify(bool $value): string;
function stringify(int|float|bool $value): string
{
return (string)$value;
}
// DO: use static value types for precise return types
function getConfig(true $asArray): array;
function getConfig(false $asArray): object;
function getConfig(bool $asArray = false): array|object
{
$data = \file_get_contents('config.json');
return \json_decode($data, $asArray);
}
// DO: keep method modifiers consistent across overloads
class Service {
public static function create(int $id): self;
public static function create(string $name): self;
public static function create(int|string $arg): self { /* ... */ }
}
Do not declare overloads without a final implementation body. Every overload set must end with a declaration that has a body.
Do not use different modifiers across overloads. All method overloads must share the same visibility and modifiers.
Do not make the implementation narrower than the overloads. The implementation must accept all argument combinations described by the overload signatures.
Do not create overloads with incompatible parameter counts unless the extra parameters have default values in the implementation.
<?tyhp
// ERROR: missing implementation with body
// function doSomething(int $a): void;
// function doSomething(string $a): void;
// ERROR: modifier mismatch across overloads
// class BadService {
// public function process(int $id): void;
// private function process(string $name): void;
// public function process(int|string $arg): void { /* ... */ }
// }
// ERROR: implementation doesn't cover string overload
// function narrow(int $a): int;
// function narrow(string $a): string;
// function narrow(int $a): int { return $a; }