Advanced Types & Callables
TypePHP allows combining generic templates (T, K, V) with high-level type algebra, including Higher-Order Callables, Lazy Iterables, Generators, Conditionals, Unions, Intersections, and Deeply Nested Containers.
Generic Callables with Template Substitution
When a function accepts a generic callback (@param callable(T): T $transformer), TypePHP dynamically substitutes T with the inferred concrete type before callback invocation:
/**
* Generic transformer function
*
* @template T
*
* @param callable(T): T $transformer
* @param T $input
*
* @return T
*/
function transformValue(callable $transformer, mixed $input): mixed
{
return $transformer($input);
}
// 1. Valid Call: Infers T = int, validates callback argument (int) and return (int)
$double = fn (int $x): int => $x * 2;
transformValue($double, 21); // Returns 42
// 2. Invalid Callback Return: T is inferred as int (from 10), but callback returns string ('invalid')
$badReturn = fn (int $x): string => 'invalid';
transformValue($badReturn, 10);
// Throws: TypeError: transformValue(): Return value must be of type int, string 'invalid' returnedHigher-Order Generic Transformers (array<K, V> & callable(V): V2)
TypePHP pre-infers template parameters across multiple arguments simultaneously:
/**
* Higher-order array mapper with 3 generic parameters
*
* @template K of array-key
* @template V
* @template V2
*
* @param callable(V): V2 $callback
* @param array<K, V> $array
*
* @return array<K, V2>
*/
function mapArray(callable $callback, array $array): array
{
$result = [];
foreach ($array as $key => $value) {
$result[$key] = $callback($value);
}
return $result;
}
$stringify = fn (int $n): string => "val_{$n}";
// 1. Valid Call: Infers K = string, V = int, V2 = string
$res = mapArray($stringify, ['a' => 10, 'b' => 20]);
// Returns: ['a' => 'val_10', 'b' => 'val_20']
// 2. Invalid Call: 'invalid_string' violates inferred V = int on function entry!
mapArray($stringify, ['item1' => 10, 'item2' => 'invalid_string']);
// Throws: TypeError: mapArray(): Argument $array['item2'] must be of type int, string 'invalid_string' givenGeneric Iterables & Generators (iterable<T> & Generator<K, V>)
TypePHP substitutes template parameters into iterators, validating yielded items, keys, and generator inputs ($gen->send()) lazily during execution:
/**
* @template T
*
* @param iterable<T> $stream
* @param T $sample
*
* @return list<T>
*/
function collectStream(iterable $stream, mixed $sample): array
{
$collected = [];
foreach ($stream as $item) {
$collected[] = $item;
}
return $collected;
}
// Infers T = int from $sample (1)
$iterator = new ArrayIterator([10, 'invalid', 30]);
collectStream($iterator, 1);
// Throws: TypeError: Iterator $stream value must be of type int, string 'invalid' givenGeneric Interactive Generators (Generator<int, T, T, void> / TSend)
When TSend uses a generic template T, $gen->send() is dynamically validated against the bound generic type:
/**
* @template T
*
* @param T $initial
*
* @return Generator<int, T, T, void>
*/
function streamInteractive(mixed $initial): Generator
{
$current = $initial;
for ($i = 0; $i < 3; $i++) {
$input = yield $i => $current;
if ($input !== null) {
$current = $input;
}
}
}
// Initial value 10 locks T = int
$gen = streamInteractive(10);
$gen->current();
$gen->send(20); // Valid (20 is int)
$gen->send('invalid'); // Invalid: string violates T = int!
// Throws: TypeError: streamInteractive(): Generator sent value (TSend) must be of type int, string 'invalid' givenConditional Return Types with Generics ((T is Dog ? A : B))
TypePHP dynamically evaluates conditional return types based on generic templates:
/**
* @template T
*
* @param T $input
* @param mixed $output
*
* @return (T is Dog ? positive-int : non-empty-string)
*/
function processInput(mixed $input, mixed $output): mixed
{
return $output;
}
// 1. T is inferred as Dog -> Evaluates return contract as positive-int
processInput(new Dog(), 100); // Valid
// 2. T is inferred as Cat -> Evaluates return contract as non-empty-string
processInput(new Cat(), 'valid_string'); // Valid
processInput(new Cat(), ''); // Invalid: empty string violates non-empty-string
// Throws: TypeError: processInput(): Return value must be of type non-empty-stringNegated Generic Conditionals ((T is not Dog ? A : B))
/**
* @template T
*
* @param T $input
* @param mixed $result
*
* @return (T is not Dog ? non-empty-string : positive-int)
*/
function processNegated(mixed $input, mixed $result): mixed
{
return $result;
}
processNegated(new Cat(), 'valid_text'); // Valid (Cat is not Dog -> non-empty-string)
processNegated(new Dog(), 42); // Valid (Dog is Dog -> positive-int)Generics with Unions and Intersections
TypePHP fully supports combining generic structures with Union (|) and Intersection (&) types:
Generic Containers Holding Unions (Collection<Dog|Cat>)
/** @var Collection<Dog|Cat> $animals */
$animals = new Collection();
$animals->add(new Dog()); // Valid
$animals->add(new Cat()); // Valid
$animals->add(new Car()); // Invalid: Car is neither Dog nor Cat
// Throws: TypeError: Collection::add(): Argument $item (template T = Dog|Cat) must be of type (Dog | Cat)Unions of Generic Containers (Producer<Dog> | Producer<Cat>)
/**
* @param Producer<Dog>|Producer<Cat> $producer
*/
function handleAnimalProducer(Producer $producer): void
{
// ...
}
handleAnimalProducer(new Producer(new Dog())); // Valid
handleAnimalProducer(new Producer(new Cat())); // Valid
handleAnimalProducer(new Producer(new Car())); // Invalid
// Throws: TypeError: Argument $producer must be of type Producer<Dog>|Producer<Cat>Generic Containers Holding Intersections (Collection<Countable & ArrayAccess>)
Enforce that generic items must implement multiple interfaces simultaneously:
/** @var Collection<Countable&ArrayAccess> $collections */
$collections = new Collection();
$collections->add(new CountableArrayAccess()); // Valid (Implements both)
$collections->add(new CountableOnly()); // Invalid (Fails ArrayAccess interface)
// Throws: TypeError: Argument $item must be of type Countable&ArrayAccessComplex Unions of Intersections in Generics
You can combine parenthesized unions and intersections inside generic parameters:
/** @var Collection<(Countable&ArrayAccess)|(Iterator&Countable)> $payload */
$payload = new Collection();
$payload->add(new CountableArrayAccess()); // Valid
$payload->add(new ArrayIterator([1, 2])); // ValidDeeply Nested Generics (Collection<Producer<Dog>>)
TypePHP recursively evaluates deeply nested generic structures down to any depth:
/** @var Collection<Producer<Dog>> $producers */
$producers = new Collection();
// Valid Addition
$producers->add(new Producer(new Dog()));
// Invalid Addition (Producer holding Car instead of Dog)
$producers->add(new Producer(new Car()));
// Throws: TypeError: Argument $item must be an instance of Producer<Dog>