From 643316a33dc6ef6341c45071416cb796449da651 Mon Sep 17 00:00:00 2001
From: Cees-Jan Kiewiet <ceesjank@gmail.com>
Date: Thu, 24 Mar 2022 08:03:54 +0100
Subject: [PATCH] Add template annotations

These annotations will aid static analyses like PHPStan and Psalm to
enhance type-safety for this project and projects depending on it

These changes make the following example understandable by PHPStan:

```php
final readonly class User
{
    public function __construct(
        public string $name,
    )
}

/**
 * \React\Promise\PromiseInterface<User>
 */
function getCurrentUserFromDatabase(): \React\Promise\PromiseInterface
{
    // The following line would do the database query and fetch the
result from it
    // but keeping it simple for the sake of the example.
    return \React\Promise\resolve(new User('WyriHaximus'));
}

// For the sake of this example we're going to assume the following code
runs
// in \React\Async\async call
echo await(getCurrentUserFromDatabase())->name; // This echos:
WyriHaximus
```
---
 README.md                 | 12 ++++----
 src/FiberMap.php          | 19 ++++++++++---
 src/functions.php         | 56 ++++++++++++++++++++++++------------
 tests/AwaitTest.php       |  2 +-
 tests/CoroutineTest.php   | 10 +++----
 tests/ParallelTest.php    |  3 ++
 tests/SeriesTest.php      |  6 ++++
 tests/WaterfallTest.php   |  6 ++++
 tests/types/async.php     | 17 +++++++++++
 tests/types/await.php     | 23 +++++++++++++++
 tests/types/coroutine.php | 60 +++++++++++++++++++++++++++++++++++++++
 tests/types/parallel.php  | 33 +++++++++++++++++++++
 tests/types/series.php    | 33 +++++++++++++++++++++
 tests/types/waterfall.php | 42 +++++++++++++++++++++++++++
 14 files changed, 288 insertions(+), 34 deletions(-)
 create mode 100644 tests/types/async.php
 create mode 100644 tests/types/await.php
 create mode 100644 tests/types/coroutine.php
 create mode 100644 tests/types/parallel.php
 create mode 100644 tests/types/series.php
 create mode 100644 tests/types/waterfall.php

diff --git a/README.md b/README.md
index 6e71f49..d8dd55c 100644
--- a/README.md
+++ b/README.md
@@ -58,7 +58,7 @@ Async\await(…);
 
 ### async()
 
-The `async(callable $function): callable` function can be used to
+The `async(callable():(PromiseInterface<T>|T) $function): (callable():PromiseInterface<T>)` function can be used to
 return an async function for a function that uses [`await()`](#await) internally.
 
 This function is specifically designed to complement the [`await()` function](#await).
@@ -226,7 +226,7 @@ await($promise);
 
 ### await()
 
-The `await(PromiseInterface $promise): mixed` function can be used to
+The `await(PromiseInterface<T> $promise): T` function can be used to
 block waiting for the given `$promise` to be fulfilled.
 
 ```php
@@ -278,7 +278,7 @@ try {
 
 ### coroutine()
 
-The `coroutine(callable $function, mixed ...$args): PromiseInterface<mixed>` function can be used to
+The `coroutine(callable(mixed ...$args):(\Generator|PromiseInterface<T>|T) $function, mixed ...$args): PromiseInterface<T>` function can be used to
 execute a Generator-based coroutine to "await" promises.
 
 ```php
@@ -498,7 +498,7 @@ Loop::addTimer(2.0, function () use ($promise): void {
 
 ### parallel()
 
-The `parallel(iterable<callable():PromiseInterface<mixed>> $tasks): PromiseInterface<array<mixed>>` function can be used
+The `parallel(iterable<callable():PromiseInterface<T>> $tasks): PromiseInterface<array<T>>` function can be used
 like this:
 
 ```php
@@ -540,7 +540,7 @@ React\Async\parallel([
 
 ### series()
 
-The `series(iterable<callable():PromiseInterface<mixed>> $tasks): PromiseInterface<array<mixed>>` function can be used
+The `series(iterable<callable():PromiseInterface<T>> $tasks): PromiseInterface<array<T>>` function can be used
 like this:
 
 ```php
@@ -582,7 +582,7 @@ React\Async\series([
 
 ### waterfall()
 
-The `waterfall(iterable<callable(mixed=):PromiseInterface<mixed>> $tasks): PromiseInterface<mixed>` function can be used
+The `waterfall(iterable<callable(mixed=):PromiseInterface<T>> $tasks): PromiseInterface<T>` function can be used
 like this:
 
 ```php
diff --git a/src/FiberMap.php b/src/FiberMap.php
index 0648788..f843a2d 100644
--- a/src/FiberMap.php
+++ b/src/FiberMap.php
@@ -6,13 +6,15 @@
 
 /**
  * @internal
+ *
+ * @template T
  */
 final class FiberMap
 {
     /** @var array<int,bool> */
     private static array $status = [];
 
-    /** @var array<int,PromiseInterface> */
+    /** @var array<int,PromiseInterface<T>> */
     private static array $map = [];
 
     /** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
@@ -27,19 +29,28 @@ public static function cancel(\Fiber $fiber): void
         self::$status[\spl_object_id($fiber)] = true;
     }
 
-    /** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
+    /**
+     * @param \Fiber<mixed,mixed,mixed,mixed> $fiber
+     * @param PromiseInterface<T> $promise
+     */
     public static function setPromise(\Fiber $fiber, PromiseInterface $promise): void
     {
         self::$map[\spl_object_id($fiber)] = $promise;
     }
 
-    /** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
+    /**
+     * @param \Fiber<mixed,mixed,mixed,mixed> $fiber
+     * @param PromiseInterface<T> $promise
+     */
     public static function unsetPromise(\Fiber $fiber, PromiseInterface $promise): void
     {
         unset(self::$map[\spl_object_id($fiber)]);
     }
 
-    /** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
+    /**
+     * @param \Fiber<mixed,mixed,mixed,mixed> $fiber
+     * @return ?PromiseInterface<T>
+     */
     public static function getPromise(\Fiber $fiber): ?PromiseInterface
     {
         return self::$map[\spl_object_id($fiber)] ?? null;
diff --git a/src/functions.php b/src/functions.php
index 5a02406..6c27936 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -176,8 +176,14 @@
  * await($promise);
  * ```
  *
- * @param callable $function
- * @return callable(mixed ...): PromiseInterface<mixed>
+ * @template T
+ * @template A1 (any number of function arguments, see https://github.com/phpstan/phpstan/issues/8214)
+ * @template A2
+ * @template A3
+ * @template A4
+ * @template A5
+ * @param callable(A1,A2,A3,A4,A5): (PromiseInterface<T>|T) $function
+ * @return callable(A1=,A2=,A3=,A4=,A5=): PromiseInterface<T>
  * @since 4.0.0
  * @see coroutine()
  */
@@ -268,8 +274,9 @@ function async(callable $function): callable
  * }
  * ```
  *
- * @param PromiseInterface $promise
- * @return mixed returns whatever the promise resolves to
+ * @template T
+ * @param PromiseInterface<T> $promise
+ * @return T returns whatever the promise resolves to
  * @throws \Exception when the promise is rejected with an `Exception`
  * @throws \Throwable when the promise is rejected with a `Throwable`
  * @throws \UnexpectedValueException when the promise is rejected with an unexpected value (Promise API v1 or v2 only)
@@ -279,6 +286,8 @@ function await(PromiseInterface $promise): mixed
     $fiber = null;
     $resolved = false;
     $rejected = false;
+
+    /** @var T $resolvedValue */
     $resolvedValue = null;
     $rejectedThrowable = null;
     $lowLevelFiber = \Fiber::getCurrent();
@@ -292,6 +301,7 @@ function (mixed $value) use (&$resolved, &$resolvedValue, &$fiber, $lowLevelFibe
             /** @var ?\Fiber<mixed,mixed,mixed,mixed> $fiber */
             if ($fiber === null) {
                 $resolved = true;
+                /** @var T $resolvedValue */
                 $resolvedValue = $value;
                 return;
             }
@@ -305,7 +315,7 @@ function (mixed $throwable) use (&$rejected, &$rejectedThrowable, &$fiber, $lowL
 
             if (!$throwable instanceof \Throwable) {
                 $throwable = new \UnexpectedValueException(
-                    'Promise rejected with unexpected value of type ' . (is_object($throwable) ? get_class($throwable) : gettype($throwable))
+                    'Promise rejected with unexpected value of type ' . (is_object($throwable) ? get_class($throwable) : gettype($throwable)) /** @phpstan-ignore-line */
                 );
 
                 // avoid garbage references by replacing all closures in call stack.
@@ -592,9 +602,16 @@ function delay(float $seconds): void
  * });
  * ```
  *
- * @param callable(mixed ...$args):(\Generator<mixed,PromiseInterface,mixed,mixed>|mixed) $function
+ * @template T
+ * @template TYield
+ * @template A1 (any number of function arguments, see https://github.com/phpstan/phpstan/issues/8214)
+ * @template A2
+ * @template A3
+ * @template A4
+ * @template A5
+ * @param callable(A1, A2, A3, A4, A5):(\Generator<mixed, PromiseInterface<TYield>, TYield, PromiseInterface<T>|T>|PromiseInterface<T>|T) $function
  * @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is
- * @return PromiseInterface<mixed>
+ * @return PromiseInterface<T>
  * @since 3.0.0
  */
 function coroutine(callable $function, mixed ...$args): PromiseInterface
@@ -611,7 +628,7 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
 
     $promise = null;
     $deferred = new Deferred(function () use (&$promise) {
-        /** @var ?PromiseInterface $promise */
+        /** @var ?PromiseInterface<T> $promise */
         if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
             $promise->cancel();
         }
@@ -632,7 +649,6 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
             return;
         }
 
-        /** @var mixed $promise */
         $promise = $generator->current();
         if (!$promise instanceof PromiseInterface) {
             $next = null;
@@ -642,6 +658,7 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
             return;
         }
 
+        /** @var PromiseInterface<TYield> $promise */
         assert($next instanceof \Closure);
         $promise->then(function ($value) use ($generator, $next) {
             $generator->send($value);
@@ -660,12 +677,13 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
 }
 
 /**
- * @param iterable<callable():PromiseInterface<mixed>> $tasks
- * @return PromiseInterface<array<mixed>>
+ * @template T
+ * @param iterable<callable():(PromiseInterface<T>|T)> $tasks
+ * @return PromiseInterface<array<T>>
  */
 function parallel(iterable $tasks): PromiseInterface
 {
-    /** @var array<int,PromiseInterface> $pending */
+    /** @var array<int,PromiseInterface<T>> $pending */
     $pending = [];
     $deferred = new Deferred(function () use (&$pending) {
         foreach ($pending as $promise) {
@@ -720,14 +738,15 @@ function parallel(iterable $tasks): PromiseInterface
 }
 
 /**
- * @param iterable<callable():PromiseInterface<mixed>> $tasks
- * @return PromiseInterface<array<mixed>>
+ * @template T
+ * @param iterable<callable():(PromiseInterface<T>|T)> $tasks
+ * @return PromiseInterface<array<T>>
  */
 function series(iterable $tasks): PromiseInterface
 {
     $pending = null;
     $deferred = new Deferred(function () use (&$pending) {
-        /** @var ?PromiseInterface $pending */
+        /** @var ?PromiseInterface<T> $pending */
         if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
             $pending->cancel();
         }
@@ -774,14 +793,15 @@ function series(iterable $tasks): PromiseInterface
 }
 
 /**
- * @param iterable<(callable():PromiseInterface<mixed>)|(callable(mixed):PromiseInterface<mixed>)> $tasks
- * @return PromiseInterface<mixed>
+ * @template T
+ * @param iterable<(callable():(PromiseInterface<T>|T))|(callable(mixed):(PromiseInterface<T>|T))> $tasks
+ * @return PromiseInterface<($tasks is non-empty-array|\Traversable ? T : null)>
  */
 function waterfall(iterable $tasks): PromiseInterface
 {
     $pending = null;
     $deferred = new Deferred(function () use (&$pending) {
-        /** @var ?PromiseInterface $pending */
+        /** @var ?PromiseInterface<T> $pending */
         if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
             $pending->cancel();
         }
diff --git a/tests/AwaitTest.php b/tests/AwaitTest.php
index 3158a1b..25e269b 100644
--- a/tests/AwaitTest.php
+++ b/tests/AwaitTest.php
@@ -413,7 +413,7 @@ public function testRejectedPromisesShouldBeDetached(callable $await): void
         })());
     }
 
-    /** @return iterable<string,list<callable(PromiseInterface): mixed>> */
+    /** @return iterable<string,list<callable(PromiseInterface<mixed>): mixed>> */
     public function provideAwaiters(): iterable
     {
         yield 'await' => [static fn (React\Promise\PromiseInterface $promise): mixed => React\Async\await($promise)];
diff --git a/tests/CoroutineTest.php b/tests/CoroutineTest.php
index 2c674c5..1df4cdc 100644
--- a/tests/CoroutineTest.php
+++ b/tests/CoroutineTest.php
@@ -22,7 +22,7 @@ public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsImmediately
     {
         $promise = coroutine(function () {
             if (false) { // @phpstan-ignore-line
-                yield;
+                yield resolve(null);
             }
             return 42;
         });
@@ -53,7 +53,7 @@ public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsImmediately()
     {
         $promise = coroutine(function () {
             if (false) { // @phpstan-ignore-line
-                yield;
+                yield resolve(null);
             }
             throw new \RuntimeException('Foo');
         });
@@ -99,7 +99,7 @@ public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsAfterYieldi
 
     public function testCoroutineReturnsRejectedPromiseIfFunctionYieldsInvalidValue(): void
     {
-        $promise = coroutine(function () {
+        $promise = coroutine(function () { // @phpstan-ignore-line
             yield 42;
         });
 
@@ -169,7 +169,7 @@ public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorRet
 
         $promise = coroutine(function () {
             if (false) { // @phpstan-ignore-line
-                yield;
+                yield resolve(null);
             }
             return 42;
         });
@@ -249,7 +249,7 @@ public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorYie
 
         gc_collect_cycles();
 
-        $promise = coroutine(function () {
+        $promise = coroutine(function () { // @phpstan-ignore-line
             yield 42;
         });
 
diff --git a/tests/ParallelTest.php b/tests/ParallelTest.php
index 37b1e10..ad24589 100644
--- a/tests/ParallelTest.php
+++ b/tests/ParallelTest.php
@@ -12,6 +12,9 @@ class ParallelTest extends TestCase
 {
     public function testParallelWithoutTasks(): void
     {
+        /**
+         * @var array<callable(): React\Promise\PromiseInterface<mixed>> $tasks
+         */
         $tasks = array();
 
         $promise = React\Async\parallel($tasks);
diff --git a/tests/SeriesTest.php b/tests/SeriesTest.php
index 9b20815..69cafd5 100644
--- a/tests/SeriesTest.php
+++ b/tests/SeriesTest.php
@@ -12,6 +12,9 @@ class SeriesTest extends TestCase
 {
     public function testSeriesWithoutTasks(): void
     {
+        /**
+         * @var array<callable(): React\Promise\PromiseInterface<mixed>> $tasks
+         */
         $tasks = array();
 
         $promise = React\Async\series($tasks);
@@ -151,6 +154,9 @@ public function testSeriesWithErrorFromInfiniteIteratorAggregateReturnsPromiseRe
         $tasks = new class() implements \IteratorAggregate {
             public int $called = 0;
 
+            /**
+             * @return \Iterator<callable(): React\Promise\PromiseInterface<mixed>>
+             */
             public function getIterator(): \Iterator
             {
                 while (true) { // @phpstan-ignore-line
diff --git a/tests/WaterfallTest.php b/tests/WaterfallTest.php
index 2b274b2..be174a9 100644
--- a/tests/WaterfallTest.php
+++ b/tests/WaterfallTest.php
@@ -12,6 +12,9 @@ class WaterfallTest extends TestCase
 {
     public function testWaterfallWithoutTasks(): void
     {
+        /**
+         * @var array<callable(): React\Promise\PromiseInterface<mixed>> $tasks
+         */
         $tasks = array();
 
         $promise = React\Async\waterfall($tasks);
@@ -165,6 +168,9 @@ public function testWaterfallWithErrorFromInfiniteIteratorAggregateReturnsPromis
         $tasks = new class() implements \IteratorAggregate {
             public int $called = 0;
 
+            /**
+             * @return \Iterator<callable(): React\Promise\PromiseInterface<mixed>>
+             */
             public function getIterator(): \Iterator
             {
                 while (true) { // @phpstan-ignore-line
diff --git a/tests/types/async.php b/tests/types/async.php
new file mode 100644
index 0000000..b5ba8fe
--- /dev/null
+++ b/tests/types/async.php
@@ -0,0 +1,17 @@
+<?php
+
+use React\Promise\PromiseInterface;
+use function PHPStan\Testing\assertType;
+use function React\Async\async;
+use function React\Async\await;
+use function React\Promise\resolve;
+
+assertType('React\Promise\PromiseInterface<bool>', async(static fn (): bool => true)());
+assertType('React\Promise\PromiseInterface<bool>', async(static fn (): PromiseInterface => resolve(true))());
+assertType('React\Promise\PromiseInterface<bool>', async(static fn (): bool => await(resolve(true)))());
+
+assertType('React\Promise\PromiseInterface<int>', async(static fn (int $a): int => $a)(42));
+assertType('React\Promise\PromiseInterface<int>', async(static fn (int $a, int $b): int => $a + $b)(10, 32));
+assertType('React\Promise\PromiseInterface<int>', async(static fn (int $a, int $b, int $c): int => $a + $b + $c)(10, 22, 10));
+assertType('React\Promise\PromiseInterface<int>', async(static fn (int $a, int $b, int $c, int $d): int => $a + $b + $c + $d)(10, 22, 5, 5));
+assertType('React\Promise\PromiseInterface<int>', async(static fn (int $a, int $b, int $c, int $d, int $e): int => $a + $b + $c + $d + $e)(10, 12, 10, 5, 5));
diff --git a/tests/types/await.php b/tests/types/await.php
new file mode 100644
index 0000000..07d51b6
--- /dev/null
+++ b/tests/types/await.php
@@ -0,0 +1,23 @@
+<?php
+
+use React\Promise\PromiseInterface;
+use function PHPStan\Testing\assertType;
+use function React\Async\async;
+use function React\Async\await;
+use function React\Promise\resolve;
+
+assertType('bool', await(resolve(true)));
+assertType('bool', await(async(static fn (): bool => true)()));
+assertType('bool', await(async(static fn (): PromiseInterface => resolve(true))()));
+assertType('bool', await(async(static fn (): bool => await(resolve(true)))()));
+
+final class AwaitExampleUser
+{
+    public string $name;
+
+    public function __construct(string $name) {
+        $this->name = $name;
+    }
+}
+
+assertType('string', await(resolve(new AwaitExampleUser('WyriHaximus')))->name);
diff --git a/tests/types/coroutine.php b/tests/types/coroutine.php
new file mode 100644
index 0000000..4c0f84c
--- /dev/null
+++ b/tests/types/coroutine.php
@@ -0,0 +1,60 @@
+<?php
+
+use function PHPStan\Testing\assertType;
+use function React\Async\await;
+use function React\Async\coroutine;
+use function React\Promise\resolve;
+
+assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
+    return true;
+}));
+
+assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
+    return resolve(true);
+}));
+
+// assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
+//     return (yield resolve(true));
+// }));
+
+assertType('React\Promise\PromiseInterface<int>', coroutine(static function () {
+//     $bool = yield resolve(true);
+//     assertType('bool', $bool);
+
+    return time();
+}));
+
+// assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
+//     $bool = yield resolve(true);
+//     assertType('bool', $bool);
+
+//     return $bool;
+// }));
+
+assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
+    yield resolve(time());
+
+    return true;
+}));
+
+assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
+    for ($i = 0; $i <= 10; $i++) {
+        yield resolve($i);
+    }
+
+    return true;
+}));
+
+assertType('React\Promise\PromiseInterface<int>', coroutine(static fn(int $a): int => $a, 42));
+assertType('React\Promise\PromiseInterface<int>', coroutine(static fn(int $a, int $b): int => $a + $b, 10, 32));
+assertType('React\Promise\PromiseInterface<int>', coroutine(static fn(int $a, int $b, int $c): int => $a + $b + $c, 10, 22, 10));
+assertType('React\Promise\PromiseInterface<int>', coroutine(static fn(int $a, int $b, int $c, int $d): int => $a + $b + $c + $d, 10, 22, 5, 5));
+assertType('React\Promise\PromiseInterface<int>', coroutine(static fn(int $a, int $b, int $c, int $d, int $e): int => $a + $b + $c + $d + $e, 10, 12, 10, 5, 5));
+
+assertType('bool', await(coroutine(static function () {
+    return true;
+})));
+
+// assertType('bool', await(coroutine(static function () {
+//     return (yield resolve(true));
+// })));
diff --git a/tests/types/parallel.php b/tests/types/parallel.php
new file mode 100644
index 0000000..dacd024
--- /dev/null
+++ b/tests/types/parallel.php
@@ -0,0 +1,33 @@
+<?php
+
+use React\Promise\PromiseInterface;
+use function PHPStan\Testing\assertType;
+use function React\Async\await;
+use function React\Async\parallel;
+use function React\Promise\resolve;
+
+assertType('React\Promise\PromiseInterface<array>', parallel([]));
+
+assertType('React\Promise\PromiseInterface<array<bool|float|int>>', parallel([
+    static fn (): PromiseInterface => resolve(true),
+    static fn (): PromiseInterface => resolve(time()),
+    static fn (): PromiseInterface => resolve(microtime(true)),
+]));
+
+assertType('React\Promise\PromiseInterface<array<bool|float|int>>', parallel([
+    static fn (): bool => true,
+    static fn (): int => time(),
+    static fn (): float => microtime(true),
+]));
+
+assertType('array<bool|float|int>', await(parallel([
+    static fn (): PromiseInterface => resolve(true),
+    static fn (): PromiseInterface => resolve(time()),
+    static fn (): PromiseInterface => resolve(microtime(true)),
+])));
+
+assertType('array<bool|float|int>', await(parallel([
+    static fn (): bool => true,
+    static fn (): int => time(),
+    static fn (): float => microtime(true),
+])));
diff --git a/tests/types/series.php b/tests/types/series.php
new file mode 100644
index 0000000..9a233e3
--- /dev/null
+++ b/tests/types/series.php
@@ -0,0 +1,33 @@
+<?php
+
+use React\Promise\PromiseInterface;
+use function PHPStan\Testing\assertType;
+use function React\Async\await;
+use function React\Async\series;
+use function React\Promise\resolve;
+
+assertType('React\Promise\PromiseInterface<array>', series([]));
+
+assertType('React\Promise\PromiseInterface<array<bool|float|int>>', series([
+    static fn (): PromiseInterface => resolve(true),
+    static fn (): PromiseInterface => resolve(time()),
+    static fn (): PromiseInterface => resolve(microtime(true)),
+]));
+
+assertType('React\Promise\PromiseInterface<array<bool|float|int>>', series([
+    static fn (): bool => true,
+    static fn (): int => time(),
+    static fn (): float => microtime(true),
+]));
+
+assertType('array<bool|float|int>', await(series([
+    static fn (): PromiseInterface => resolve(true),
+    static fn (): PromiseInterface => resolve(time()),
+    static fn (): PromiseInterface => resolve(microtime(true)),
+])));
+
+assertType('array<bool|float|int>', await(series([
+    static fn (): bool => true,
+    static fn (): int => time(),
+    static fn (): float => microtime(true),
+])));
diff --git a/tests/types/waterfall.php b/tests/types/waterfall.php
new file mode 100644
index 0000000..1470785
--- /dev/null
+++ b/tests/types/waterfall.php
@@ -0,0 +1,42 @@
+<?php
+
+use React\Promise\PromiseInterface;
+use function PHPStan\Testing\assertType;
+use function React\Async\await;
+use function React\Async\waterfall;
+use function React\Promise\resolve;
+
+assertType('React\Promise\PromiseInterface<null>', waterfall([]));
+
+assertType('React\Promise\PromiseInterface<float>', waterfall([
+    static fn (): PromiseInterface => resolve(microtime(true)),
+]));
+
+assertType('React\Promise\PromiseInterface<float>', waterfall([
+    static fn (): float => microtime(true),
+]));
+
+// Desired, but currently unsupported with the current set of templates
+//assertType('React\Promise\PromiseInterface<float>', waterfall([
+//    static fn (): PromiseInterface => resolve(true),
+//    static fn (bool $bool): PromiseInterface => resolve(time()),
+//    static fn (int $int): PromiseInterface => resolve(microtime(true)),
+//]));
+
+assertType('float', await(waterfall([
+    static fn (): PromiseInterface => resolve(microtime(true)),
+])));
+
+// Desired, but currently unsupported with the current set of templates
+//assertType('float', await(waterfall([
+//    static fn (): PromiseInterface => resolve(true),
+//    static fn (bool $bool): PromiseInterface => resolve(time()),
+//    static fn (int $int): PromiseInterface => resolve(microtime(true)),
+//])));
+
+// assertType('React\Promise\PromiseInterface<null>', waterfall(new EmptyIterator()));
+
+$iterator = new ArrayIterator([
+    static fn (): PromiseInterface => resolve(true),
+]);
+assertType('React\Promise\PromiseInterface<bool>', waterfall($iterator));