Repository: true-myth/true-myth
Pull request #902 (2025-01-07 17:10:40)

Task: add `all`, `allSettled`, `any`, `race`, `timer`, and `timeout`
Introduce both static combinators and an instance method for combining different `Task` instances.

Whereas the existing static methods allow constructing individual `Task` instances and the existing instance methods allow coordinating across a sequence of `Task` instance, these static methods and their equivalent module-level functions allow composing `Task` instances concurrently.

The new `timeout` method is a convenience for `someTask.timeout(123)` or `someTask.timeout(anExistingTimer)`, and produces a new `Task` which resolves or rejects to `someTask` if it settles first or rejects to a timeout error if the time passed elapses.


Patch:
diff --git a/src/task.ts b/src/task.ts
index 8199df2..028e23d 100644
--- a/src/task.ts
+++ b/src/task.ts
@@ -476,7 +476,7 @@ class TaskImpl<T, E> implements PromiseLike<Result<T, E>> {
     `andThen` to combine two functions which *both* create a `Task` from an
     unwrapped type.
 
-    The [`Promise.prototype.then`][then] method a helpful comparison: if you
+    The [`Promise.prototype.then`][then] method is a helpful comparison: if you
     have a `Promise`, you can pass its `then` method a callback which returns
     another `Promise`, and the result will not be a *nested* promise, but a
     single `Promise`. The difference is that `Promise.prototype.then` unwraps
@@ -698,6 +698,26 @@ class TaskImpl<T, E> implements PromiseLike<Result<T, E>> {
     );
   }
 
+  /**
+    Attempt to run this {@linkcode Task} to completion, but stop if the passed
+    {@linkcode Timer}, or one constructed from a passed time in milliseconds,
+    elapses first.
+
+    If this `Task` and the duration happen to have the same duration, `timeout`
+    will favor this `Task` over the timeout.
+
+    @param timer A {@linkcode Timer}
+    @returns A `Task` which has the resolution value of `this` or a `Timeout`
+      if the timer elapsed.
+   */
+  timeout(timer: Timer): Task<T, E | Timeout>;
+  timeout(ms: number): Task<T, E | Timeout>;
+  timeout(timerOrMs: Timer | number): Task<unknown, unknown> {
+    let timerTask = typeof timerOrMs === 'number' ? timer(timerOrMs) : timerOrMs;
+    let timeout = timerTask.andThen((ms) => Task.reject(new Timeout(ms)));
+    return race([this as Task<T, E>, timeout]);
+  }
+
   /**
     Get the underlying `Promise`. Useful when you need to work with an
     API which *requires* a `Promise`, rather than a `PromiseLike`.
@@ -718,6 +738,363 @@ class TaskImpl<T, E> implements PromiseLike<Result<T, E>> {
   }
 }
 
+/**
+  An unknown {@linkcode Task}. This is a private type utility; it is only
+  exported for the sake of internal tests.
+
+  @internal
+ */
+export type AnyTask = Task<unknown, unknown>;
+
+export type TaskTypesFor<A extends readonly AnyTask[]> = [
+  { -readonly [P in keyof A]: ResolvesTo<A[P]> },
+  { -readonly [P in keyof A]: RejectsWith<A[P]> },
+];
+
+/**
+  The resolution type for a given {@linkcode Task}.
+  @internal
+ */
+export type ResolvesTo<T extends AnyTask> = T extends Task<infer T, infer _> ? T : never;
+
+/**
+  The rejection type for a given {@linkcode Task}
+  @internal
+ */
+export type RejectsWith<T extends AnyTask> = T extends Task<infer _, infer E> ? E : never;
+
+/**
+  Create a {@linkcode Task} which will resolve to {@linkcode Unit} after a set
+  interval. (Safely wraps [`setTimeout`][setTimeout].)
+
+  [setTimeout]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout
+
+  This can be combined with the {@linkcode Task.timeout} instance method.
+
+  @param ms The number of milliseconds to wait before resolving the `Task`.
+  @returns a Task which resolves to the passed-in number of milliseconds.
+ */
+export function timer(ms: number): Timer {
+  return new Task((resolve) => setTimeout(() => resolve(ms), ms)) as Timer;
+}
+
+/**
+  A type utility for mapping an input array of tasks into the appropriate output
+  for `all`.
+
+  @internal
+ */
+export type All<A extends readonly AnyTask[]> = Task<
+  Array<TaskTypesFor<A>[0][number]>,
+  TaskTypesFor<A>[1][number]
+>;
+
+/**
+  Given an array of tasks, return a new `Task` which resolves once all tasks
+  successfully resolve or any task rejects.
+
+  ## Examples
+
+  Once all tasks resolve:
+
+  ```ts
+  import { all, timer } from 'true-myth/task';
+
+  let allTasks = all([
+    timer(10),
+    timer(100),
+    timer(1_000),
+  ]);
+
+  let result = await allTasks;
+  console.log(result.toString()); // [Ok(10,100,1000)]
+  ```
+
+  If any tasks do *not* resolve:
+
+  ```ts
+  let { task: willReject, reject } = Task.withResolvers<never, string>();
+
+  let allTasks = all([
+    timer(10),
+    timer(20),
+    willReject,
+  ]);
+
+  reject("something went wrong");
+  let result = await allTasks;
+  console.log(result.toString()); // Err("something went wrong")
+  ```
+
+  @param tasks The list of tasks to wait on.
+*/
+export function all(tasks: []): Task<[], never>;
+export function all<A extends readonly AnyTask[]>(tasks: A): All<A>;
+export function all<A extends readonly AnyTask[]>(tasks: A): Task<unknown, unknown> {
+  if (tasks.length === 0) {
+    return Task.resolve([]);
+  }
+
+  let total = tasks.length;
+  let oks = new Array<unknown>();
+  let hasRejected = false;
+
+  return new Task((resolve, reject) => {
+    // Because all tasks will *always* resolve, we need to manage this manually,
+    // rather than using `Promise.all`, so that we produce a rejected `Task` as
+    // soon as *any* `Task` rejects.
+    for (let task of tasks) {
+      // Instead, each `Task` wires up handlers for resolution and rejection.
+      task.match({
+        // If it rejected, then check whether one of the other tasks has already
+        // rejected. If so, there is nothing to do. Otherwise, *this* task is
+        // the first to reject, so we reject the overall `Task` with the reason
+        // for this one, and flag that the `Task` is rejected.
+        Rejected: (reason) => {
+          if (hasRejected) {
+            return;
+          }
+
+          hasRejected = true;
+          reject(reason);
+        },
+
+        // If it resolved, the same rule applies if one of the other tasks has
+        // rejected, because the`Task` for this `any` will already be rejected
+        // with that task’s rejection reason. Otherwise, we will add this value
+        // to the bucket of resolutions, and track whether *all* the tasks have
+        // resolved. If or when we get to that point, we resolve with the full
+        // set of values.
+        Resolved: (value) => {
+          if (hasRejected) {
+            return;
+          }
+
+          oks.push(value);
+          if (oks.length === total) {
+            resolve(oks);
+          }
+        },
+      });
+    }
+  });
+}
+
+/**
+  @internal
+*/
+export type Settled<A extends readonly AnyTask[]> = {
+  -readonly [P in keyof A]: Result<ResolvesTo<A[P]>, RejectsWith<A[P]>>;
+};
+
+/**
+  Given an array of tasks, return a new {@linkcode Task} which resolves once all
+  of the tasks have either resolved or rejected. The resulting `Task` is a tuple
+  or array corresponding exactly to the tasks passed in, either resolved or
+  rejected.
+
+  ## Example
+
+  Given a mix of resolving and rejecting tasks:
+
+  ```ts
+  let settledTask = allSettled([
+    Task.resolve<string, number>("hello"),
+    Task.reject<number, boolean>(true),
+    Task.resolve<{ fancy: boolean }>, Error>({ fancy: true }),
+  ]);
+
+  let output = await settledTask;
+  if (output.isOk) { // always true, not currently statically knowable
+    for (let result of output.value) {
+      console.log(result.toString());
+    }
+  }
+  ```
+
+  The resulting output will be:
+
+  ```
+  Ok("hello"),
+  Err(true),
+  Ok({ fancy: true }),
+  ```
+
+  @param tasks The tasks to wait on settling.
+ */
+export function allSettled<A extends readonly AnyTask[]>(tasks: A): Task<Settled<A>, never>;
+export function allSettled(tasks: AnyTask[]): Task<unknown, never> {
+  // All task promises should resolve; none should ever reject, by definition.
+  // The “settled” state here is represented by the `Task` itself, *not* by the
+  // `Promise` rejection. This means the logic of `allSettled` is actually just
+  // `Promise.all`!
+  return new Task((resolve) => {
+    Promise.all(tasks).then(resolve);
+  });
+}
+
+/**
+  Given an array of tasks, return a new {@linkcode Task} which resolves once
+  _any_ of the tasks resolves successfully, or which rejects once _all_ the
+  tasks have rejected.
+
+  ## Examples
+
+  When any item resolves:
+
+  ```ts
+  import { any, timer } from 'true-myth/task';
+
+  let anyTask = any([
+    timer(20),
+    timer(10),
+    timer(30),
+  ]);
+
+  let result = await anyTask;
+  console.log(result.toString()); // Ok(10);
+  ```
+
+  When all items reject:
+
+  ```ts
+  import Task, { timer } from 'true-myth/task';
+
+  let anyTask = any([
+    timer(20).andThen((time) => Task.reject(`${time}ms`)),
+    timer(10).andThen((time) => Task.reject(`${time}ms`)),
+    timer(30).andThen((time) => Task.reject(`${time}ms`)),
+  ]);
+
+  let result = await anyTask;
+  console.log(result.toString()); // Err(AggregateRejection: `Task.race`: 10ms,20ms,30ms)
+  ```
+
+  (Note that the order in the resulting `AggregateRejection` is not guaranteed
+  to be stable!)
+
+  @param tasks The set of tasks to check for any resolution.
+  @returns A Task which is either {@linkcode Resolved} with the value of the
+    first task to resolve, or {@linkcode Rejected} with the rejection reasons
+    for all the tasks passed in in an {@linkcode AggregateRejection}. Note that
+    the order of the rejection reasons is not guaranteed.
+*/
+export function any(tasks: []): Task<never, AggregateRejection<[]>>;
+export function any<A extends readonly AnyTask[]>(
+  tasks: A
+): Task<TaskTypesFor<A>[0][number], AggregateRejection<Array<TaskTypesFor<A>[1][number]>>>;
+export function any(tasks: [] | AnyTask[]): AnyTask {
+  if (tasks.length === 0) {
+    return Task.reject(new AggregateRejection([]));
+  }
+
+  let total = tasks.length;
+  let hasResolved = false;
+  let rejections = new Array<unknown>();
+
+  return new Task((resolve, reject) => {
+    // We cannot use `Promise.any`, because it will only return the first `Task`
+    // that resolves, and the `Promise` for a `Task` *always* either resolves if
+    // it settles.
+    for (let task of tasks) {
+      // Instead, each `Task` wires up handlers for resolution and rejection.
+      task.match({
+        // If it resolved, then check whether one of the other tasks has already
+        // resolved. If so, there is nothing to do. Otherwise, *this* task is
+        // the first to resolve, so we resolve the overall `Task` with the value
+        // for this one, and flag that the `Task` is resolved.
+        Resolved: (value) => {
+          if (hasResolved) {
+            return;
+          }
+
+          hasResolved = true;
+          resolve(value);
+        },
+
+        // If it rejected, the same rule applies if one of the other tasks has
+        // successfully resolved, because the`Task` for this `any` will already
+        // have resolved to that task. Otherwise, we will add this rejection to
+        // the bucket of rejections, and track whether *all* the tasks have
+        // rejected. If or when we get to that point, we reject with the full
+        // set of rejections.
+        Rejected: (reason) => {
+          if (hasResolved) {
+            return;
+          }
+
+          rejections.push(reason);
+
+          if (rejections.length === total) {
+            reject(new AggregateRejection(rejections));
+          }
+        },
+      });
+    }
+  });
+}
+
+/**
+  Given an array of tasks, produce a new {@linkcode Task} which will resolve or
+  reject with the resolution or rejection of the *first* task which settles.
+
+  ## Example
+
+  ```ts
+  import Task, { race } from 'true-myth/task';
+
+  let { task: task1, resolve } = Task.withResolvers();
+  let task2 = new Task((_resolve) => {});
+  let task3 = new Task((_resolve) => {});
+
+  resolve("Cool!");
+  let theResult = await race([task1, task2, task3]);
+  console.log(theResult.toString()); // Ok("Cool!")
+  ```
+
+  @param tasks The tasks to race against each other.
+ */
+export function race(tasks: []): Task<never, never>;
+export function race<A extends readonly AnyTask[]>(
+  tasks: A
+): Task<TaskTypesFor<A>[0][number], TaskTypesFor<A>[1][number]>;
+export function race(tasks: [] | AnyTask[]): AnyTask {
+  if (tasks.length === 0) {
+    return new Task(() => {
+      /* pending forever, just like `Promise.race` */
+    });
+  }
+
+  return new Task((resolve, reject) => {
+    Promise.race(tasks).then((result) =>
+      result.match({
+        Ok: resolve,
+        Err: reject,
+      })
+    );
+  });
+}
+
+/**
+  An error type produced when {@linkcode any} produces any rejections. All
+  rejections are aggregated into this type.
+
+  > [!NOTE]
+  > This error type is not allowed to be subclassed.
+*/
+export class AggregateRejection<E extends unknown[]> extends Error {
+  readonly name = 'AggregateRejection';
+
+  constructor(readonly errors: E) {
+    super('`Task.race`');
+  }
+
+  toString() {
+    let internalMessage = this.errors.length > 0 ? `[${safeToString(this.errors)}]` : 'No tasks';
+    return super.toString() + `: ${internalMessage}`;
+  }
+}
+
 /** @group Task Variants */
 export interface Pending<T, E> extends Omit<TaskImpl<T, E>, 'value' | 'reason'> {
   get isPending(): true;
@@ -820,10 +1197,10 @@ export class InvalidAccess extends Error {
   }
 }
 
-/** {@inheritdoc Task.tryOr} */
+/** @inheritdoc Task.tryOr */
 export const tryOr = TaskImpl.tryOr;
 
-/** {@inheritdoc Task.tryOrElse} */
+/** @inheritdoc Task.tryOrElse */
 export const tryOrElse = TaskImpl.tryOrElse;
 
 /* v8 ignore next 3 */
@@ -915,3 +1292,41 @@ export const Task = TaskImpl as TaskConstructor;
  */
 export type Task<T, E> = Pending<T, E> | Resolved<T, E> | Rejected<T, E>;
 export default Task;
+
+// Branded timer type.
+declare const PhantomData: unique symbol;
+
+/** @internal */
+export declare class Phantom<T extends PropertyKey> {
+  private readonly [PhantomData]: T;
+}
+
+/**
+  A {@linkcode Task} specialized for use with {@linkcode timeout} or other
+  methods or functions which want to know they are using.
+
+  > [!NOTE]
+  > This type has zero runtime overhead, including for construction: it is just
+  > a `Task` with additional *type information*.
+ */
+export type Timer = Task<number, never> & Phantom<'Timer'>;
+
+/**
+  An `Error` type representing a timeout, as when a {@linkcode Timer} elapses.
+ */
+class Timeout extends Error {
+  readonly #duration: number;
+
+  get duration(): number {
+    return this.#duration;
+  }
+
+  constructor(readonly ms: number) {
+    super(`Timed out after ${ms} milliseconds`);
+    this.#duration = ms;
+  }
+}
+
+// Export *only* the type side (at least until we hear of a reason to do
+// otherwise): people ought not be subclassing or instantiating `Timeout`!
+export type { Timeout };


Test patch:
diff --git a/test/task.test.ts b/test/task.test.ts
index e5ac67b..256008b 100644
--- a/test/task.test.ts
+++ b/test/task.test.ts
@@ -7,6 +7,14 @@ import Task, {
   State,
   TaskExecutorException,
   UnsafePromise,
+  all,
+  allSettled,
+  any,
+  timer,
+  race,
+  Settled,
+  AggregateRejection,
+  Timer,
 } from 'true-myth/task';
 import Result from 'true-myth/result';
 import Unit from 'true-myth/unit';
@@ -515,213 +523,209 @@ describe('`Task`', () => {
           expect(unwrapErr(result)).toEqual(theReason);
         });
       });
+    });
 
-      describe('`or`', () => {
-        describe('when the first Task resolves', async () => {
-          test('when the second is pending', async () => {
-            let theFirst = Task.resolve(123);
-            let theSecond = new Task<number, never>(noOp);
+    describe('`or`', () => {
+      describe('when the first Task resolves', async () => {
+        test('when the second is pending', async () => {
+          let theFirst = Task.resolve(123);
+          let theSecond = new Task<number, never>(noOp);
+
+          let theChain = theFirst.or(theSecond);
+          expect(theFirst.state).toBe(State.Resolved);
+          expect(theSecond.state).toBe(State.Pending);
+          expect(theChain.state).toBe(State.Pending);
+          await theFirst;
+          expect(theFirst.state).toBe(State.Resolved);
+          expect(theSecond.state).toBe(State.Pending);
+          expect(theChain.state).toBe(State.Resolved);
+        });
 
-            let theChain = theFirst.or(theSecond);
-            expect(theFirst.state).toBe(State.Resolved);
-            expect(theSecond.state).toBe(State.Pending);
-            expect(theChain.state).toBe(State.Pending);
-            await theFirst;
-            expect(theFirst.state).toBe(State.Resolved);
-            expect(theSecond.state).toBe(State.Pending);
-            expect(theChain.state).toBe(State.Resolved);
-          });
+        test('when the second Task resolves', async () => {
+          let theTask = Task.resolve(123).or(Task.resolve(456));
+          expectTypeOf(theTask).toEqualTypeOf<Task<number, never>>();
+          let theResult = await theTask;
+          expect(unwrap(theResult)).toEqual(123);
+        });
 
-          test('when the second Task resolves', async () => {
-            let theTask = Task.resolve(123).or(Task.resolve(456));
-            expectTypeOf(theTask).toEqualTypeOf<Task<number, never>>();
-            let theResult = await theTask;
-            expect(unwrap(theResult)).toEqual(123);
-          });
+        test('when the second Task rejects', async () => {
+          let theReason = 'hello';
+          let theTask = Task.resolve<number, string>(123).or(
+            Task.reject<number, string>(theReason)
+          );
+          expectTypeOf(theTask).toEqualTypeOf<Task<number, string>>();
+          let theResult = await theTask;
+          expect(unwrap(theResult)).toEqual(123);
+        });
+      });
 
-          test('when the second Task rejects', async () => {
-            let theReason = 'hello';
-            let theTask = Task.resolve<number, string>(123).or(
-              Task.reject<number, string>(theReason)
-            );
-            expectTypeOf(theTask).toEqualTypeOf<Task<number, string>>();
-            let theResult = await theTask;
-            expect(unwrap(theResult)).toEqual(123);
-          });
+      describe('when the first Task rejects', () => {
+        test('when the second is pending', async () => {
+          let theFirst = Task.reject<unknown, string>('blergh');
+          let theSecond = new Task<number, never>(noOp);
+
+          let theChain = theFirst.or(theSecond);
+          expect(theFirst.state).toBe(State.Rejected);
+          expect(theSecond.state).toBe(State.Pending);
+          expect(theChain.state).toBe(State.Pending);
+          await theFirst;
+          expect(theFirst.state).toBe(State.Rejected);
+          expect(theSecond.state).toBe(State.Pending);
+          expect(theChain.state).toBe(State.Pending);
         });
 
-        describe('when the first Task rejects', () => {
-          test('when the second is pending', async () => {
-            let theFirst = Task.reject<unknown, string>('blergh');
-            let theSecond = new Task<number, never>(noOp);
+        test('when the second Task resolves', async () => {
+          let theTask = Task.reject<string, number>(123).or(Task.resolve<string, number>('hello'));
+          expectTypeOf(theTask).toEqualTypeOf<Task<string, number>>();
+          let theResult = await theTask;
+          expect(unwrap(theResult)).toBe('hello');
+        });
 
-            let theChain = theFirst.or(theSecond);
-            expect(theFirst.state).toBe(State.Rejected);
-            expect(theSecond.state).toBe(State.Pending);
-            expect(theChain.state).toBe(State.Pending);
-            await theFirst;
-            expect(theFirst.state).toBe(State.Rejected);
-            expect(theSecond.state).toBe(State.Pending);
-            expect(theChain.state).toBe(State.Pending);
-          });
+        test('when the second Task rejects', async () => {
+          let theReason = 123;
+          let theTask = Task.reject<string, number>(theReason).or(Task.reject<string, number>(456));
+          expectTypeOf(theTask).toEqualTypeOf<Task<string, number>>();
+          let theResult = await theTask;
+          expect(unwrapErr(theResult)).toEqual(456);
+        });
+      });
 
-          test('when the second Task resolves', async () => {
-            let theTask = Task.reject<string, number>(123).or(
-              Task.resolve<string, number>('hello')
-            );
-            expectTypeOf(theTask).toEqualTypeOf<Task<string, number>>();
-            let theResult = await theTask;
-            expect(unwrap(theResult)).toBe('hello');
-          });
+      // Matches the text in the docs.
+      test('all combinations', async () => {
+        let resolvedA = Task.resolve<string, string>('A');
+        let resolvedB = Task.resolve<string, string>('B');
+        let rejectedA = Task.reject<string, string>('bad');
+        let rejectedB = Task.reject<string, string>('lame');
 
-          test('when the second Task rejects', async () => {
-            let theReason = 123;
-            let theTask = Task.reject<string, number>(theReason).or(
-              Task.reject<string, number>(456)
-            );
-            expectTypeOf(theTask).toEqualTypeOf<Task<string, number>>();
-            let theResult = await theTask;
-            expect(unwrapErr(theResult)).toEqual(456);
-          });
-        });
+        let aOrB = resolvedA.or(resolvedB);
+        await aOrB;
 
-        // Matches the text in the docs.
-        test('all combinations', async () => {
-          let resolvedA = Task.resolve<string, string>('A');
-          let resolvedB = Task.resolve<string, string>('B');
-          let rejectedA = Task.reject<string, string>('bad');
-          let rejectedB = Task.reject<string, string>('lame');
+        let aOrRA = resolvedA.or(rejectedA);
+        await aOrRA;
 
-          let aOrB = resolvedA.or(resolvedB);
-          await aOrB;
+        let raOrA = rejectedA.or(resolvedA);
+        await raOrA;
 
-          let aOrRA = resolvedA.or(rejectedA);
-          await aOrRA;
+        let raOrRb = rejectedA.or(rejectedB);
+        await raOrRb;
 
-          let raOrA = rejectedA.or(resolvedA);
-          await raOrA;
+        expect(aOrB.toString()).toEqual('Task.Resolved("A")');
+        expect(aOrRA.toString()).toEqual('Task.Resolved("A")');
+        expect(raOrA.toString()).toEqual('Task.Resolved("A")');
+        expect(raOrRb.toString()).toEqual('Task.Rejected("lame")');
+      });
+    });
 
-          let raOrRb = rejectedA.or(rejectedB);
-          await raOrRb;
+    describe('`orElse`', () => {
+      test('for a pending promise', async () => {
+        let theTask = new Task<number, string>(noOp).orElse((reason) =>
+          Task.resolve(reason.length)
+        );
+        expectTypeOf(theTask).toEqualTypeOf<Task<number, never>>();
 
-          expect(aOrB.toString()).toEqual('Task.Resolved("A")');
-          expect(aOrRA.toString()).toEqual('Task.Resolved("A")');
-          expect(raOrA.toString()).toEqual('Task.Resolved("A")');
-          expect(raOrRb.toString()).toEqual('Task.Rejected("lame")');
-        });
+        expect(theTask.state).toBe(State.Pending);
       });
 
-      describe('`orElse`', () => {
-        test('for a pending promise', async () => {
-          let theTask = new Task<number, string>(noOp).orElse((reason) =>
+      describe('when the first `Task` resolves', () => {
+        test('when the second is pending', async () => {
+          let theFirst = Task.resolve<number, string>(123);
+          let theSecond = new Task<number, boolean>(noOp);
+          let theChain = theFirst.orElse(() => theSecond);
+
+          expect(theFirst.state).toBe(State.Resolved);
+          expect(theSecond.state).toBe(State.Pending);
+          expect(theChain.state).toBe(State.Pending);
+          await theFirst;
+          expect(theFirst.state).toBe(State.Resolved);
+          expect(theSecond.state).toBe(State.Pending);
+          expect(theChain.state).toBe(State.Resolved);
+        });
+
+        test('when the second `Task` resolves', async () => {
+          let theTask = Task.resolve<number, string>(123).orElse((reason) =>
             Task.resolve(reason.length)
           );
           expectTypeOf(theTask).toEqualTypeOf<Task<number, never>>();
 
-          expect(theTask.state).toBe(State.Pending);
+          let result = await theTask;
+          expect(unwrap(result)).toBe(123);
         });
 
-        describe('when the first `Task` resolves', () => {
-          test('when the second is pending', async () => {
-            let theFirst = Task.resolve<number, string>(123);
-            let theSecond = new Task<number, boolean>(noOp);
-            let theChain = theFirst.orElse(() => theSecond);
-
-            expect(theFirst.state).toBe(State.Resolved);
-            expect(theSecond.state).toBe(State.Pending);
-            expect(theChain.state).toBe(State.Pending);
-            await theFirst;
-            expect(theFirst.state).toBe(State.Resolved);
-            expect(theSecond.state).toBe(State.Pending);
-            expect(theChain.state).toBe(State.Resolved);
-          });
-
-          test('when the second `Task` resolves', async () => {
-            let theTask = Task.resolve<number, string>(123).orElse((reason) =>
-              Task.resolve(reason.length)
-            );
-            expectTypeOf(theTask).toEqualTypeOf<Task<number, never>>();
-
-            let result = await theTask;
-            expect(unwrap(result)).toBe(123);
-          });
-
-          test('when the second `Task` rejects', async () => {
-            let theTask = Task.resolve<number, string>(123).orElse((reason) =>
-              Task.reject(reason.length)
-            );
-            expectTypeOf(theTask).toEqualTypeOf<Task<number, number>>();
+        test('when the second `Task` rejects', async () => {
+          let theTask = Task.resolve<number, string>(123).orElse((reason) =>
+            Task.reject(reason.length)
+          );
+          expectTypeOf(theTask).toEqualTypeOf<Task<number, number>>();
 
-            let result = await theTask;
-            expect(unwrap(result)).toBe(123);
-          });
+          let result = await theTask;
+          expect(unwrap(result)).toBe(123);
         });
+      });
 
-        describe('when the first `Task` rejects', () => {
-          test('when the second is pending', async () => {
-            let theFirst = Task.reject<number, string>('teh sads');
-            let theSecond = new Task<number, boolean>(noOp);
-            let theChain = theFirst.orElse(() => theSecond);
-
-            expect(theFirst.state).toBe(State.Rejected);
-            expect(theSecond.state).toBe(State.Pending);
-            expect(theChain.state).toBe(State.Pending);
-            await theFirst;
-            expect(theFirst.state).toBe(State.Rejected);
-            expect(theSecond.state).toBe(State.Pending);
-            expect(theChain.state).toBe(State.Pending);
-          });
+      describe('when the first `Task` rejects', () => {
+        test('when the second is pending', async () => {
+          let theFirst = Task.reject<number, string>('teh sads');
+          let theSecond = new Task<number, boolean>(noOp);
+          let theChain = theFirst.orElse(() => theSecond);
+
+          expect(theFirst.state).toBe(State.Rejected);
+          expect(theSecond.state).toBe(State.Pending);
+          expect(theChain.state).toBe(State.Pending);
+          await theFirst;
+          expect(theFirst.state).toBe(State.Rejected);
+          expect(theSecond.state).toBe(State.Pending);
+          expect(theChain.state).toBe(State.Pending);
+        });
 
-          test('when the second `Task` resolves', async () => {
-            let theTask = Task.reject<number, string>('nope').orElse((reason) =>
-              Task.resolve<number, string>(reason.length)
-            );
-            expectTypeOf(theTask).toEqualTypeOf<Task<number, string>>();
+        test('when the second `Task` resolves', async () => {
+          let theTask = Task.reject<number, string>('nope').orElse((reason) =>
+            Task.resolve<number, string>(reason.length)
+          );
+          expectTypeOf(theTask).toEqualTypeOf<Task<number, string>>();
 
-            let result = await theTask;
-            expect(unwrap(result)).toBe(4);
-          });
+          let result = await theTask;
+          expect(unwrap(result)).toBe(4);
+        });
 
-          test('when the second `Task` rejects', async () => {
-            let theTask = Task.reject<number, string>('first error').orElse((reason) =>
-              Task.reject<number, boolean>(reason.includes("'"))
-            );
-            expectTypeOf(theTask).toEqualTypeOf<Task<number, boolean>>();
+        test('when the second `Task` rejects', async () => {
+          let theTask = Task.reject<number, string>('first error').orElse((reason) =>
+            Task.reject<number, boolean>(reason.includes("'"))
+          );
+          expectTypeOf(theTask).toEqualTypeOf<Task<number, boolean>>();
 
-            let result = await theTask;
-            expect(unwrapErr(result)).toBe(false);
-          });
+          let result = await theTask;
+          expect(unwrapErr(result)).toBe(false);
         });
       });
+    });
 
-      describe('`match`', () => {
-        test('with a resolved task', async () => {
-          await Task.tryOrElse(Promise.resolve(123), stringify).match({
-            Resolved: (value) => expect(value).toBe(123),
-            Rejected: (_reason) => expect.unreachable(),
-          });
-          expect.assertions(1);
+    describe('`match`', () => {
+      test('with a resolved task', async () => {
+        await Task.tryOrElse(Promise.resolve(123), stringify).match({
+          Resolved: (value) => expect(value).toBe(123),
+          Rejected: (_reason) => expect.unreachable(),
         });
+        expect.assertions(1);
+      });
 
-        test('with a rejected task', async () => {
-          await Task.tryOrElse(Promise.reject(123), stringify).match({
-            Resolved: (_value) => expect.unreachable(),
-            Rejected: (reason) => expect(reason).toEqual(stringify(123)),
-          });
-          expect.assertions(1);
+      test('with a rejected task', async () => {
+        await Task.tryOrElse(Promise.reject(123), stringify).match({
+          Resolved: (_value) => expect.unreachable(),
+          Rejected: (reason) => expect(reason).toEqual(stringify(123)),
         });
+        expect.assertions(1);
+      });
 
-        test('with a pending task', () => {
-          // Will never resolve, but should be collected at the end of the test.
-          // Note that this test passes when, and only when, no test assertions
-          // run at all.
-          let task = new Task(() => {});
-          task.match({
-            Resolved: () => expect.unreachable(),
-            Rejected: () => expect.unreachable(),
-          });
-          expect.assertions(0);
+      test('with a pending task', () => {
+        // Will never resolve, but should be collected at the end of the test.
+        // Note that this test passes when, and only when, no test assertions
+        // run at all.
+        let task = new Task(() => {});
+        task.match({
+          Resolved: () => expect.unreachable(),
+          Rejected: () => expect.unreachable(),
         });
+        expect.assertions(0);
       });
     });
 
@@ -770,6 +774,76 @@ describe('`Task`', () => {
         expect(unwrap(theResult)).toEqual(theValue);
       });
     });
+
+    describe('`timeout`', () => {
+      describe('with a number', () => {
+        test('that is shorter', async () => {
+          // shorter by dint of "literally any timeout is shorter than never".
+          let { task } = Task.withResolvers<string, never>();
+          let result = await task.timeout(1);
+          expect(result.isErr).toBe(true);
+          if (result.isErr) {
+            expect(result.error.duration).toBe(1);
+          } else {
+            expect.unreachable();
+          }
+        });
+
+        test('that is equal', async () => {
+          let duration = 1;
+          let task = new Task((resolve) => setTimeout(() => resolve(duration), duration)).timeout(
+            duration
+          );
+          let result = await task;
+          expect(result.isOk).toBe(true);
+          expect(unwrap(result)).toBe(duration);
+        });
+
+        test('that is longer', async () => {
+          let duration = 1;
+          let task = new Task((resolve) => setTimeout(() => resolve(duration), duration)).timeout(
+            duration * 2
+          );
+          let result = await task;
+          expect(result.isOk).toBe(true);
+          expect(unwrap(result)).toBe(duration);
+        });
+      });
+
+      describe('with another timer', () => {
+        test('that is shorter', async () => {
+          // shorter by dint of "literally any timeout is shorter than never".
+          let { task } = Task.withResolvers<string, never>();
+          let result = await task.timeout(timer(1));
+          expect(result.isErr).toBe(true);
+          if (result.isErr) {
+            expect(result.error.duration).toBe(1);
+          } else {
+            expect.unreachable();
+          }
+        });
+
+        test('that is equal', async () => {
+          let duration = 1;
+          let task = new Task((resolve) => setTimeout(() => resolve(duration), duration)).timeout(
+            timer(duration)
+          );
+          let result = await task;
+          expect(result.isOk).toBe(true);
+          expect(unwrap(result)).toBe(duration);
+        });
+
+        test('that is longer', async () => {
+          let duration = 1;
+          let task = new Task((resolve) => setTimeout(() => resolve(duration), duration)).timeout(
+            timer(duration * 2)
+          );
+          let result = await task;
+          expect(result.isOk).toBe(true);
+          expect(unwrap(result)).toBe(duration);
+        });
+      });
+    });
   });
 
   describe('accessors', () => {
@@ -916,6 +990,799 @@ describe('`Task`', () => {
   });
 });
 
+describe('module-scope functions', () => {
+  describe('all', () => {
+    describe('with a single task', () => {
+      test('that is still pending', () => {
+        let { task } = Task.withResolvers<string, number>();
+        let result = all([task] as const);
+        expectTypeOf(result).toEqualTypeOf<Task<Array<string>, number>>();
+        expect(result.state).toBe(State.Pending);
+      });
+
+      test('that has resolved', async () => {
+        let theTask = Task.resolve('hello');
+        let result = all([theTask]);
+        expectTypeOf(result).toEqualTypeOf<Task<string[], never>>();
+        await result;
+        expect(result.state).toBe(State.Resolved);
+        if (result.isResolved) {
+          expect(result.value).toEqual(['hello']);
+        }
+      });
+
+      test('that has rejected', async () => {
+        let theReason = 'oops';
+        let theTask = Task.reject<string, string>(theReason);
+        let result = all([theTask]);
+        await result;
+        expectTypeOf(result).toEqualTypeOf<Task<string[], string>>();
+        expect(result.state).toBe(State.Rejected);
+        if (result.isRejected) {
+          expect(result.reason).toBe(theReason);
+        }
+      });
+    });
+
+    describe('with two tasks', () => {
+      test('types', () => {
+        let { task: task1 } = Task.withResolvers<string, number>();
+        let { task: task2 } = Task.withResolvers<boolean, Error>();
+        let fromTuple = all([task1, task2] as const);
+        let fromArray = all([task1, task2]);
+        expectTypeOf(fromTuple).toEqualTypeOf(fromArray);
+      });
+
+      test('that are all still pending', () => {
+        let { task: task1 } = Task.withResolvers<string, number>();
+        let { task: task2 } = Task.withResolvers<boolean, Error>();
+        let result = all([task1, task2] as const);
+        expectTypeOf(result).toEqualTypeOf<Task<Array<string | boolean>, number | Error>>();
+        expect(result.state).toBe(State.Pending);
+      });
+
+      describe('when the first resolves', () => {
+        test('while the second is pending', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<string, number>();
+          let { task: task2 } = Task.withResolvers<number, boolean>();
+          let result = all([task1, task2] as const);
+          expectTypeOf(result).toEqualTypeOf<Task<Array<string | number>, number | boolean>>();
+
+          resolve1('first');
+          expect(result.state).toBe(State.Pending);
+        });
+
+        test('when the second has already resolved', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<number, string>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<string, boolean>();
+          let result = all([task1, task2]);
+          expectTypeOf(result).toEqualTypeOf<Task<Array<number | string>, string | boolean>>();
+
+          resolve2('second');
+          resolve1(1);
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value).toEqual(['second', 1]);
+          }
+        });
+
+        test('when the second has rejected', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<string, number>();
+          let { task: task2, reject: reject2 } = Task.withResolvers<boolean, string>();
+          let result = all([task1, task2] as const);
+          expectTypeOf(result).toEqualTypeOf<Task<Array<string | boolean>, number | string>>();
+
+          reject2('error');
+          resolve1('first');
+          await result;
+          expect(result.state).toBe(State.Rejected);
+          if (result.isRejected) {
+            expect(result.reason).toBe('error');
+          }
+        });
+      });
+
+      describe('when the second resolves', () => {
+        test('while the first is pending', async () => {
+          let { task: task1 } = Task.withResolvers<number, boolean>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<string, Error>();
+          let result = all([task1, task2] as const);
+          expectTypeOf(result).toEqualTypeOf<Task<Array<number | string>, boolean | Error>>();
+
+          resolve2('second');
+          expect(result.state).toBe(State.Pending);
+        });
+
+        test('when the first has already resolved', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<string, number>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<number, boolean>();
+          let result = all([task1, task2] as const);
+          expectTypeOf(result).toEqualTypeOf<Task<Array<string | number>, number | boolean>>();
+
+          resolve1('first');
+          resolve2(2);
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value).toEqual(['first', 2]);
+          }
+        });
+
+        test('when the first has rejected', async () => {
+          let { task: task1, reject: reject1 } = Task.withResolvers<number, string>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<string, boolean>();
+          let result = all([task1, task2] as const);
+          expectTypeOf(result).toEqualTypeOf<Task<Array<number | string>, string | boolean>>();
+
+          reject1('error');
+          resolve2('second');
+          await result;
+          expect(result.state).toBe(State.Rejected);
+          if (result.isRejected) {
+            expect(result.reason).toBe('error');
+          }
+        });
+      });
+    });
+
+    test('rejection happens immediately', async () => {
+      let { task: task1, reject: reject1 } = Task.withResolvers<number, string>();
+      let { task: task2 } = Task.withResolvers<string, boolean>();
+      let result = all([task1, task2] as const);
+      expectTypeOf(result).toEqualTypeOf<Task<Array<number | string>, string | boolean>>();
+
+      reject1('error');
+      await result;
+      expect(result.state).toBe(State.Rejected);
+      if (result.isRejected) {
+        expect(result.reason).toBe('error');
+      }
+    });
+
+    // Covers the early return path when a second rejection triggers.
+    test('multiple rejections', async () => {
+      let result = all([Task.reject('first'), Task.reject('second')]);
+      await result;
+      if (result.isRejected) {
+        expect(result.reason).toBe('first');
+      } else {
+        expect.unreachable();
+      }
+    });
+
+    test('with an empty set of tasks', async () => {
+      let noTasks = all([]);
+      expectTypeOf(noTasks).toEqualTypeOf<Task<[], never>>();
+      await noTasks;
+      expect(noTasks.state).toBe(State.Resolved);
+      if (noTasks.isResolved) {
+        expect(noTasks.value).toEqual([]);
+      }
+    });
+
+    test('with multiple tasks (integration)', async () => {
+      let { task: willReject, reject } = Task.withResolvers();
+
+      let allTasks = all([timer(10), timer(20), willReject]);
+
+      let theReason = 'something went wrong';
+      reject(theReason);
+      let result = await allTasks;
+      expect(allTasks.state).toBe(State.Rejected);
+      if (allTasks.isRejected) {
+        expect(allTasks.reason).toBe(theReason);
+      }
+
+      expect(result.isErr).toBe(true);
+      if (result.isErr) {
+        expect(result.error).toBe(theReason);
+      } else {
+        expect.unreachable();
+      }
+    });
+  });
+
+  describe('allSettled', () => {
+    describe('with a single task', () => {
+      test('that is still pending', () => {
+        let { task } = Task.withResolvers<string, number>();
+        let result = allSettled([task] as const);
+        expectTypeOf(result).toEqualTypeOf<Task<[Result<string, number>], never>>();
+        expect(result.state).toBe(State.Pending);
+      });
+
+      test('that has resolved', async () => {
+        let theTask = Task.resolve('hello');
+        let result = allSettled([theTask]);
+        expectTypeOf(result).toEqualTypeOf<Task<Array<Result<string, never>>, never>>();
+        await result;
+        expect(result.state).toBe(State.Resolved);
+        if (result.isResolved) {
+          expect(result.value[0]!.isOk).toBe(true);
+          expect(unwrap(result.value[0]!)).toBe('hello');
+        }
+      });
+
+      test('that has rejected', async () => {
+        let theReason = 'oops';
+        let theTask = Task.reject<string, string>(theReason);
+        let result = allSettled([theTask] as const);
+        await result;
+        expectTypeOf(result).toEqualTypeOf<Task<[Result<string, string>], never>>();
+        expect(result.state).toBe(State.Resolved);
+        if (result.isResolved) {
+          expect(result.value[0].isErr).toBe(true);
+          expect(unwrapErr(result.value[0])).toBe(theReason);
+        }
+      });
+    });
+
+    describe('with two tasks', () => {
+      test('that are all still pending', () => {
+        let { task: task1 } = Task.withResolvers<string, number>();
+        let { task: task2 } = Task.withResolvers<boolean, Error>();
+        let result = allSettled([task1, task2] as const);
+        expectTypeOf(result).toEqualTypeOf<
+          Task<[Result<string, number>, Result<boolean, Error>], never>
+        >();
+        expect(result.state).toBe(State.Pending);
+      });
+
+      describe('when the first resolves', () => {
+        test('while the second is pending', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<string, number>();
+          let { task: task2 } = Task.withResolvers<number, boolean>();
+          let result = allSettled([task1, task2] as const);
+          expectTypeOf(result).toEqualTypeOf<
+            Task<[Result<string, number>, Result<number, boolean>], never>
+          >();
+
+          resolve1('first');
+          expect(result.state).toBe(State.Pending);
+        });
+
+        test('when the second has already resolved', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<number, string>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<string, boolean>();
+          let result = allSettled([task1, task2] as const);
+          expectTypeOf(result).toEqualTypeOf<
+            Task<[Result<number, string>, Result<string, boolean>], never>
+          >();
+
+          resolve2('second');
+          resolve1(1);
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value[0].isOk).toBe(true);
+            expect(result.value[1].isOk).toBe(true);
+            expect(unwrap(result.value[0])).toBe(1);
+            expect(unwrap(result.value[1])).toBe('second');
+          }
+        });
+
+        test('when the second has rejected', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<string, number>();
+          let { task: task2, reject: reject2 } = Task.withResolvers<boolean, string>();
+          let result = allSettled([task1, task2] as const);
+          expectTypeOf(result).toEqualTypeOf<
+            Task<[Result<string, number>, Result<boolean, string>], never>
+          >();
+
+          reject2('error');
+          resolve1('first');
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value[0].isOk).toBe(true);
+            expect(result.value[1].isErr).toBe(true);
+            expect(unwrap(result.value[0])).toBe('first');
+            expect(unwrapErr(result.value[1])).toBe('error');
+          }
+        });
+      });
+
+      describe('when the second resolves', () => {
+        test('while the first is pending', async () => {
+          let { task: task1 } = Task.withResolvers<number, boolean>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<string, Error>();
+          let result = allSettled([task1, task2] as const);
+          expectTypeOf(result).toEqualTypeOf<
+            Task<[Result<number, boolean>, Result<string, Error>], never>
+          >();
+
+          resolve2('second');
+          expect(result.state).toBe(State.Pending);
+        });
+
+        test('when the first has already resolved', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<string, number>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<number, boolean>();
+          let result = allSettled([task1, task2] as const);
+          expectTypeOf(result).toEqualTypeOf<
+            Task<[Result<string, number>, Result<number, boolean>], never>
+          >();
+
+          resolve1('first');
+          resolve2(2);
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value[0].isOk).toBe(true);
+            expect(result.value[1].isOk).toBe(true);
+            expect(unwrap(result.value[0])).toBe('first');
+            expect(unwrap(result.value[1])).toBe(2);
+          }
+        });
+
+        test('when the first has rejected', async () => {
+          let { task: task1, reject: reject1 } = Task.withResolvers<number, string>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<string, boolean>();
+          let result = allSettled([task1, task2] as const);
+          expectTypeOf(result).toEqualTypeOf<
+            Task<[Result<number, string>, Result<string, boolean>], never>
+          >();
+
+          reject1('error');
+          resolve2('second');
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value[0].isErr).toBe(true);
+            expect(result.value[1].isOk).toBe(true);
+            expect(unwrapErr(result.value[0])).toBe('error');
+            expect(unwrap(result.value[1])).toBe('second');
+          }
+        });
+      });
+    });
+  });
+
+  describe('any', () => {
+    test('with an empty array', async () => {
+      let result = any([]);
+      expectTypeOf(result).toEqualTypeOf<Task<never, AggregateRejection<[]>>>();
+      await result;
+      expect(result.state).toBe(State.Rejected);
+      if (result.isRejected) {
+        expect(result.reason).toBeInstanceOf(AggregateRejection);
+        expect(result.reason.errors.length).toBe(0);
+        expect(result.reason.toString()).toMatch('No tasks');
+      }
+    });
+
+    describe('with a single task', () => {
+      test('that is still pending', () => {
+        let { task } = Task.withResolvers();
+        let result = any([task]);
+        expect(result.state).toBe(State.Pending);
+      });
+
+      test('that has resolved', async () => {
+        let theTask = Task.resolve('hello');
+        let result = any([theTask]);
+        await result;
+        expect(result.state).toBe(State.Resolved);
+        if (result.isResolved) {
+          expect(result.value).toBe('hello');
+        }
+      });
+
+      test('that has rejected', async () => {
+        let theReason = 'oops';
+        let theTask = Task.reject<string, string>(theReason);
+        let result = any([theTask]);
+        await result;
+        expect(result.state).toBe(State.Rejected);
+        if (result.isRejected) {
+          expect(result.reason.errors[0]).toBe(theReason);
+          expect(result.reason.toString()).toMatch('[oops]');
+        }
+      });
+    });
+
+    describe('with two tasks', () => {
+      test('that are all still pending', () => {
+        let { task: task1 } = Task.withResolvers();
+        let { task: task2 } = Task.withResolvers();
+        let result = any([task1, task2]);
+        expect(result.state).toBe(State.Pending);
+      });
+
+      describe('when the first resolves', () => {
+        test('while the second is pending', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<string, number>();
+          let { task: task2 } = Task.withResolvers<number, boolean>();
+          let result = any([task1, task2]);
+          expectTypeOf(result).toEqualTypeOf<
+            Task<string | number, AggregateRejection<Array<number | boolean>>>
+          >();
+
+          resolve1('first');
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value).toBe('first');
+          }
+        });
+
+        test('when the second has already resolved', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<number, string>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<string, boolean>();
+          let result = any([task1, task2]);
+          expectTypeOf(result).toEqualTypeOf<
+            Task<number | string, AggregateRejection<Array<string | boolean>>>
+          >();
+
+          resolve2('second');
+          resolve1(1);
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value).toBe('second');
+          }
+        });
+
+        test('when the second has rejected', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<string, number>();
+          let { task: task2, reject: reject2 } = Task.withResolvers<boolean, string>();
+          let result = any([task1, task2]);
+          expectTypeOf(result).toEqualTypeOf<
+            Task<string | boolean, AggregateRejection<Array<number | string>>>
+          >();
+
+          reject2('error');
+          resolve1('first');
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value).toBe('first');
+          }
+        });
+      });
+
+      describe('when the first rejects', () => {
+        test('while the second is pending', async () => {
+          let { task: task1, reject: reject1 } = Task.withResolvers<string, number>();
+          let { task: task2 } = Task.withResolvers<number, boolean>();
+          let result = any([task1, task2]);
+          expectTypeOf(result).toEqualTypeOf<
+            Task<string | number, AggregateRejection<Array<number | boolean>>>
+          >();
+
+          reject1(1);
+          expect(result.state).toBe(State.Pending);
+        });
+
+        test('when the second has already resolved', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<number, string>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<string, boolean>();
+          let result = any([task1, task2]);
+          expectTypeOf(result).toEqualTypeOf<
+            Task<number | string, AggregateRejection<Array<string | boolean>>>
+          >();
+
+          resolve2('second');
+          resolve1(1);
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value).toBe('second');
+          }
+        });
+
+        test('when the second has also rejected', async () => {
+          let { task: task1, reject: reject1 } = Task.withResolvers<string, number>();
+          let { task: task2, reject: reject2 } = Task.withResolvers<boolean, string>();
+          let result = any([task1, task2]);
+          expectTypeOf(result).toEqualTypeOf<
+            Task<string | boolean, AggregateRejection<Array<number | string>>>
+          >();
+
+          reject2('error');
+          reject1(1);
+          await result;
+          expect(result.state).toBe(State.Rejected);
+          if (result.isRejected) {
+            expect(result.reason).toBeInstanceOf(AggregateRejection);
+            expect(result.reason.errors[0]!).toBe('error');
+            expect(result.reason.errors[1]!).toBe(1);
+          }
+        });
+      });
+
+      describe('when the second resolves', () => {
+        test('while the first is pending', async () => {
+          let { task: task1 } = Task.withResolvers<number, boolean>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<string, Error>();
+          let result = any([task1, task2]);
+          expectTypeOf(result).toEqualTypeOf<
+            Task<number | string, AggregateRejection<Array<boolean | Error>>>
+          >();
+
+          resolve2('second');
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value).toBe('second');
+          }
+        });
+
+        test('when the first has already resolved', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<string, number>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<number, boolean>();
+          let result = any([task1, task2]);
+          expectTypeOf(result).toEqualTypeOf<
+            Task<string | number, AggregateRejection<Array<number | boolean>>>
+          >();
+
+          resolve1('first');
+          resolve2(2);
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value).toBe('first');
+          }
+        });
+
+        test('when the first has rejected', async () => {
+          let { task: task1, reject: reject1 } = Task.withResolvers<number, string>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<string, boolean>();
+          let result = any([task1, task2]);
+          expectTypeOf(result).toEqualTypeOf<
+            Task<number | string, AggregateRejection<Array<string | boolean>>>
+          >();
+
+          reject1('error');
+          resolve2('second');
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value).toBe('second');
+          }
+        });
+      });
+    });
+
+    describe('when the second rejects', () => {
+      test('while the first is pending', async () => {
+        let { task: task1 } = Task.withResolvers<number, boolean>();
+        let { task: task2, reject: reject2 } = Task.withResolvers<string, number>();
+        let result = any([task1, task2]);
+        expectTypeOf(result).toEqualTypeOf<
+          Task<number | string, AggregateRejection<Array<boolean | number>>>
+        >();
+
+        reject2(2);
+        expect(result.state).toBe(State.Pending);
+      });
+
+      test('when the first has already resolved', async () => {
+        let { task: task1, resolve: resolve1 } = Task.withResolvers<string, number>();
+        let { task: task2, reject: reject2 } = Task.withResolvers<number, boolean>();
+        let result = any([task1, task2]);
+        expectTypeOf(result).toEqualTypeOf<
+          Task<string | number, AggregateRejection<Array<number | boolean>>>
+        >();
+
+        resolve1('first');
+        reject2(true);
+        await result;
+        expect(result.state).toBe(State.Resolved);
+        if (result.isResolved) {
+          expect(result.value).toBe('first');
+        }
+      });
+
+      test('when the first has also rejected', async () => {
+        let { task: task1, reject: reject1 } = Task.withResolvers<number, string>();
+        let { task: task2, reject: reject2 } = Task.withResolvers<string, boolean>();
+        let result = any([task1, task2] as const);
+        expectTypeOf(result).toEqualTypeOf<
+          Task<number | string, AggregateRejection<Array<string | boolean>>>
+        >();
+
+        reject1('error');
+        reject2(true);
+        await result;
+        expect(result.state).toBe(State.Rejected);
+        if (result.isRejected) {
+          expect(result.reason).toBeInstanceOf(AggregateRejection);
+          expect(result.reason.errors.length).toBe(2);
+          expect(result.reason.errors[0]).toBe('error');
+          expect(result.reason.errors[1]).toBe(true);
+        }
+      });
+    });
+  });
+
+  describe('race', () => {
+    expectTypeOf(race([Task.resolve('hello'), Task.resolve(123)] as const)).toEqualTypeOf(
+      race([Task.resolve('hello'), Task.resolve(123)])
+    );
+
+    test('with an empty array', () => {
+      // Note: this will *never* resolve, so do not attempt to await it!
+      let result = race([]);
+      expectTypeOf(result).toEqualTypeOf<Task<never, never>>();
+      expect(result.state).toBe(State.Pending);
+    });
+
+    describe('with a single task', () => {
+      test('that is still pending', () => {
+        let { task } = Task.withResolvers<string, number>();
+        let result = race([task]);
+        expectTypeOf(result).toEqualTypeOf<Task<string, number>>();
+        expect(result.state).toBe(State.Pending);
+      });
+
+      test('that has resolved', async () => {
+        let theTask = Task.resolve('hello');
+        let result = race([theTask]);
+        expectTypeOf(result).toEqualTypeOf<Task<string, never>>();
+        await result;
+        expect(result.state).toBe(State.Resolved);
+        if (result.isResolved) {
+          expect(result.value).toBe('hello');
+        }
+      });
+
+      test('that has rejected', async () => {
+        let theReason = 'oops';
+        let theTask = Task.reject<string, string>(theReason);
+        let result = race([theTask]);
+        await result;
+        expectTypeOf(result).toEqualTypeOf<Task<string, string>>();
+        expect(result.state).toBe(State.Rejected);
+        if (result.isRejected) {
+          expect(result.reason).toBe(theReason);
+        }
+      });
+    });
+
+    describe('with two tasks', () => {
+      test('that are all still pending', () => {
+        let { task: task1 } = Task.withResolvers<string, number>();
+        let { task: task2 } = Task.withResolvers<boolean, Error>();
+        let result = race([task1, task2]);
+        expectTypeOf(result).toEqualTypeOf<Task<string | boolean, number | Error>>();
+        expect(result.state).toBe(State.Pending);
+      });
+
+      describe('when the first resolves', () => {
+        test('while the second is pending', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<string, number>();
+          let { task: task2 } = Task.withResolvers<number, boolean>();
+          let result = race([task1, task2]);
+          expectTypeOf(result).toEqualTypeOf<Task<string | number, number | boolean>>();
+
+          resolve1('first');
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value).toBe('first');
+          }
+        });
+
+        test('when the second has already resolved', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<number, string>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<string, boolean>();
+          let result = race([task1, task2]);
+          expectTypeOf(result).toEqualTypeOf<Task<number | string, string | boolean>>();
+
+          resolve2('second');
+          resolve1(1);
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value).toBe('second');
+          }
+        });
+
+        test('when the second has rejected', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<string, number>();
+          let { task: task2, reject: reject2 } = Task.withResolvers<boolean, string>();
+          let result = race([task1, task2]);
+          expectTypeOf(result).toEqualTypeOf<Task<string | boolean, number | string>>();
+
+          reject2('error');
+          resolve1('first');
+          await result;
+          expect(result.state).toBe(State.Rejected);
+          if (result.isRejected) {
+            expect(result.reason).toBe('error');
+          }
+        });
+      });
+
+      describe('when the second resolves', () => {
+        test('while the first is pending', async () => {
+          let { task: task1 } = Task.withResolvers<number, boolean>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<string, Error>();
+          let result = race([task1, task2]);
+          expectTypeOf(result).toEqualTypeOf<Task<number | string, boolean | Error>>();
+
+          resolve2('second');
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value).toBe('second');
+          }
+        });
+
+        test('when the first has already resolved', async () => {
+          let { task: task1, resolve: resolve1 } = Task.withResolvers<string, number>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<number, boolean>();
+          let result = race([task1, task2]);
+          expectTypeOf(result).toEqualTypeOf<Task<string | number, number | boolean>>();
+
+          resolve1('first');
+          resolve2(2);
+          await result;
+          expect(result.state).toBe(State.Resolved);
+          if (result.isResolved) {
+            expect(result.value).toBe('first');
+          }
+        });
+
+        test('when the first has rejected', async () => {
+          let { task: task1, reject: reject1 } = Task.withResolvers<number, string>();
+          let { task: task2, resolve: resolve2 } = Task.withResolvers<string, boolean>();
+          let result = race([task1, task2]);
+          expectTypeOf(result).toEqualTypeOf<Task<number | string, string | boolean>>();
+
+          reject1('error');
+          resolve2('second');
+          await result;
+          expect(result.state).toBe(State.Rejected);
+          if (result.isRejected) {
+            expect(result.reason).toBe('error');
+          }
+        });
+      });
+    });
+  });
+
+  test('timer', async () => {
+    let ms = 1;
+    let aTimer = timer(ms);
+    expectTypeOf(aTimer).toEqualTypeOf<Timer>();
+    let result = await aTimer;
+    expect(unwrap(result)).toEqual(ms);
+  });
+});
+
+describe('type utilities', () => {
+  test('results for array of tasks', () => {
+    expectTypeOf<Settled<[Task<string, number>]>>().toEqualTypeOf<[Result<string, number>]>();
+    expectTypeOf<Settled<[Task<string, number>, Task<number, string>]>>().toEqualTypeOf<
+      [Result<string, number>, Result<number, string>]
+    >;
+    expectTypeOf<
+      Settled<
+        [
+          Task<string, number>,
+          Task<number, string>,
+          Task<boolean, Error>,
+          Task<{ complicatedObjectStuff: string[] }, Error>,
+        ]
+      >
+    >().toEqualTypeOf<
+      [
+        Result<string, number>,
+        Result<number, string>,
+        Result<boolean, Error>,
+        Result<{ complicatedObjectStuff: string[] }, Error>,
+      ]
+    >();
+
+    expectTypeOf<
+      Settled<Array<Task<string, number> | Task<number, string> | Task<boolean, Error>>>
+    >().toEqualTypeOf<Array<Result<string | number | boolean, number | string | Error>>>();
+  });
+});
+
 // Supports our current targets (which do not include `Promise.withResolvers`).
 function deferred<T, E>(): {
   promise: Promise<T>;