Repository: true-myth/true-myth
Pull request #918 (2025-01-11 16:26:38)

Task: module-scoped versions of instance methods
Introducing these brings the `Task` module to full parity with the `Maybe` and `Result` modules. As the documentation notes, however, these are not great to use in practice because they require explicitly setting the types at the call site, like so

```ts
import * as Task from 'true-myth/task';
let toLength = Task.map<string, number, Error>((s) => s.length);
```

This is annoying, but I think it is worth having for the sake of parity. I suspect we may want to remove all auto-currying at some point in the not-too-distant future, because it just does not work very well given the limits of TypeScript’s type inference. Alas!


Patch:
diff --git a/src/task.ts b/src/task.ts
index a6c21d7..8d4e55a 100644
--- a/src/task.ts
+++ b/src/task.ts
@@ -720,13 +720,12 @@ class TaskImpl<T, E> implements PromiseLike<Result<T, E>> {
     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}
+    @param timerOrMs A {@linkcode Timer} or a number of milliseconds to wait for
+      this task before timing out.
     @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> {
+  timeout(timerOrMs: Timer | number): Task<T, E | Timeout> {
     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]);
@@ -1878,6 +1877,221 @@ export function safeNullable<
     });
 }
 
+/**
+  Auto-curried, standalone function form of
+  {@linkcode Task.map Task.prototype.map}.
+
+  > [!TIP]
+  > This is provided for parity with the similar functions that the `Maybe`
+  > and `Result` modules provide. However, like `Result`, you will likely find
+  > that this form is somewhat difficult to use, because TypeScript’s type
+  > inference does not support it well: you will tend to end up with an awful
+  > lot of `unknown` unless you write the type parameters explicitly at the call
+  > site.
+
+  @template T The type of the value when the `Task` resolves successfully.
+  @template E The type of the rejection reason when the `Task` rejects.
+ */
+export function map<T, U, E>(mapFn: (t: T) => U): (task: Task<T, E>) => Task<U, E>;
+export function map<T, U, E>(mapFn: (t: T) => U, task: Task<T, E>): Task<U, E>;
+export function map<T, U, E>(
+  mapFn: (t: T) => U,
+  task?: Task<T, E>
+): Task<U, E> | ((task: Task<T, E>) => Task<U, E>) {
+  return curry1((task) => task.map(mapFn), task);
+}
+
+/**
+  Auto-curried, standalone function form of
+  {@linkcode Task.mapRejected Task.prototype.mapRejected}.
+
+  > [!TIP]
+  > This is provided for parity with the similar functions that the `Maybe`
+  > and `Result` modules provide. However, like `Result`, you will likely find
+  > that this form is somewhat difficult to use, because TypeScript’s type
+  > inference does not support it well: you will tend to end up with an awful
+  > lot of `unknown` unless you write the type parameters explicitly at the call
+  > site.
+
+  @template T The type of the value when the `Task` resolves successfully.
+  @template E The type of the rejection reason when the `Task` rejects.
+ */
+export function mapRejected<T, E, F>(mapFn: (e: E) => F): (task: Task<T, E>) => Task<T, F>;
+export function mapRejected<T, E, F>(mapFn: (e: E) => F, task: Task<T, E>): Task<T, F>;
+export function mapRejected<T, E, F>(
+  mapFn: (e: E) => F,
+  task?: Task<T, E>
+): Task<T, F> | ((task: Task<T, E>) => Task<T, F>) {
+  return curry1((task) => task.mapRejected(mapFn), task);
+}
+
+/**
+  Auto-curried, standalone function form of
+  {@linkcode Task.and Task.prototype.and}.
+
+  > [!TIP]
+  > This is provided for parity with the similar functions that the `Maybe`
+  > and `Result` modules provide. However, like `Result`, you will likely find
+  > that this form is somewhat difficult to use, because TypeScript’s type
+  > inference does not support it well: you will tend to end up with an awful
+  > lot of `unknown` unless you write the type parameters explicitly at the call
+  > site.
+
+  @template T The type of the value when the `Task` resolves successfully.
+  @template E The type of the rejection reason when the `Task` rejects.
+ */
+export function and<T, U, E>(andTask: Task<U, E>): (task: Task<T, E>) => Task<U, E>;
+export function and<T, U, E>(andTask: Task<U, E>, task: Task<T, E>): Task<U, E>;
+export function and<T, U, E>(
+  andTask: Task<U, E>,
+  task?: Task<T, E>
+): Task<U, E> | ((task: Task<T, E>) => Task<U, E>) {
+  return curry1((task) => task.and(andTask), task);
+}
+
+/**
+  Auto-curried, standalone function form of
+  {@linkcode Task.andThen Task.prototype.andThen}.
+
+  > [!TIP]
+  > This is provided for parity with the similar functions that the `Maybe`
+  > and `Result` modules provide. However, like `Result`, you will likely find
+  > that this form is somewhat difficult to use, because TypeScript’s type
+  > inference does not support it well: you will tend to end up with an awful
+  > lot of `unknown` unless you write the type parameters explicitly at the call
+  > site.
+
+  @template T The type of the value when the `Task` resolves successfully.
+  @template E The type of the rejection reason when the `Task` rejects.
+ */
+export function andThen<T, U, E, F = E>(
+  thenFn: (t: T) => Task<U, F>
+): (task: Task<T, E>) => Task<U, E | F>;
+export function andThen<T, U, E, F = E>(
+  thenFn: (t: T) => Task<U, F>,
+  task: Task<T, E>
+): Task<U, E | F>;
+export function andThen<T, U, E, F = E>(
+  thenFn: (t: T) => Task<U, F>,
+  task?: Task<T, E>
+): Task<U, E | F> | ((task: Task<T, E>) => Task<U, E | F>) {
+  return curry1((task) => task.andThen(thenFn), task);
+}
+
+/**
+  Auto-curried, standalone function form of
+  {@linkcode Task.or Task.prototype.or}.
+
+  > [!TIP]
+  > This is provided for parity with the similar functions that the `Maybe`
+  > and `Result` modules provide. However, like `Result`, you will likely find
+  > that this form is somewhat difficult to use, because TypeScript’s type
+  > inference does not support it well: you will tend to end up with an awful
+  > lot of `unknown` unless you write the type parameters explicitly at the call
+  > site.
+
+  @template T The type of the value when the `Task` resolves successfully.
+  @template E The type of the rejection reason when the `Task` rejects.
+ */
+export function or<U, F, T, E>(other: Task<U, F>): (task: Task<T, E>) => Task<T | U, F>;
+export function or<U, F, T, E>(other: Task<U, F>, task: Task<T, E>): Task<T | U, F>;
+export function or<U, F, T, E>(
+  other: Task<U, F>,
+  task?: Task<T, E>
+): Task<T | U, F> | ((task: Task<T, E>) => Task<T | U, F>) {
+  return curry1((task) => task.or(other), task);
+}
+
+/**
+  Auto-curried, standalone function form of
+  {@linkcode Task.orElse Task.prototype.orElse}.
+
+  > [!TIP]
+  > This is provided for parity with the similar functions that the `Maybe`
+  > and `Result` modules provide. However, like `Result`, you will likely find
+  > that this form is somewhat difficult to use, because TypeScript’s type
+  > inference does not support it well: you will tend to end up with an awful
+  > lot of `unknown` unless you write the type parameters explicitly at the call
+  > site.
+
+  @template T The type of the value when the `Task` resolves successfully.
+  @template E The type of the rejection reason when the `Task` rejects.
+ */
+export function orElse<T, E, F, U = T>(
+  elseFn: (reason: E) => Task<U, F>
+): (task: Task<T, E>) => Task<T | U, F>;
+export function orElse<T, E, F, U = T>(
+  elseFn: (reason: E) => Task<U, F>,
+  task: Task<T, E>
+): Task<T | U, F>;
+export function orElse<T, E, F, U = T>(
+  elseFn: (reason: E) => Task<U, F>,
+  task?: Task<T, E>
+): Task<T | U, F> | ((task: Task<T, E>) => Task<T | U, F>) {
+  return curry1((task) => task.orElse(elseFn), task);
+}
+
+/**
+  Auto-curried, standalone function form of
+  {@linkcode Task.match Task.prototype.match}.
+
+  > [!TIP]
+  > This is provided for parity with the similar functions that the `Maybe`
+  > and `Result` modules provide. However, like `Result`, you will likely find
+  > that this form is somewhat difficult to use, because TypeScript’s type
+  > inference does not support it well: you will tend to end up with an awful
+  > lot of `unknown` unless you write the type parameters explicitly at the call
+  > site.
+
+  @template T The type of the value when the `Task` resolves successfully.
+  @template E The type of the rejection reason when the `Task` rejects.
+ */
+export function match<T, E, A>(matcher: Matcher<T, E, A>): (task: Task<T, E>) => Promise<A>;
+export function match<T, E, A>(matcher: Matcher<T, E, A>, task: Task<T, E>): Promise<A>;
+export function match<T, E, A>(
+  matcher: Matcher<T, E, A>,
+  task?: Task<T, E>
+): Promise<A> | ((task: Task<T, E>) => Promise<A>) {
+  return curry1((task) => task.match(matcher), task);
+}
+
+/**
+  Auto-curried, standalone function form of
+  {@linkcode Task.timeout Task.prototype.timeout}.
+
+  > [!TIP]
+  > This is provided for parity with the similar functions that the `Maybe`
+  > and `Result` modules provide. However, like `Result`, you will likely find
+  > that this form is somewhat difficult to use, because TypeScript’s type
+  > inference does not support it well: you will tend to end up with an awful
+  > lot of `unknown` unless you write the type parameters explicitly at the call
+  > site.
+
+  @template T The type of the value when the `Task` resolves successfully.
+  @template E The type of the rejection reason when the `Task` rejects.
+ */
+export function timeout<T, E>(
+  timerOrMs: Timer | number
+): (task: Task<T, E>) => Task<T, E | Timeout>;
+export function timeout<T, E>(timerOrMs: Timer | number, task: Task<T, E>): Task<T, E | Timeout>;
+export function timeout<T, E>(
+  timerOrMs: Timer | number,
+  task?: Task<T, E>
+): Task<T, E | Timeout> | ((task: Task<T, E>) => Task<T, E | Timeout>) {
+  return curry1((task) => task.timeout(timerOrMs), task);
+}
+
+/**
+  Auto-curried, standalone function form of
+  {@linkcode Task.toPromise Task.prototype.toPromise}.
+
+  @template T The type of the value when the `Task` resolves successfully.
+  @template E The type of the rejection reason when the `Task` rejects.
+ */
+export function toPromise<T, E>(task: Task<T, E>) {
+  return task.toPromise();
+}
+
 function identity<T>(value: T): T {
   return value;
 }


Test patch:
diff --git a/test/task.test.ts b/test/task.test.ts
index 9877bac..4aa0c96 100644
--- a/test/task.test.ts
+++ b/test/task.test.ts
@@ -26,6 +26,16 @@ import Task, {
   resolve,
   reject,
   withResolvers,
+  orElse,
+  match,
+  or,
+  andThen,
+  and,
+  mapRejected,
+  map,
+  timeout,
+  Timeout,
+  toPromise,
 } from 'true-myth/task';
 import Maybe from 'true-myth/maybe';
 import Result from 'true-myth/result';
@@ -2327,6 +2337,610 @@ describe('module-scope functions', () => {
       expect(theTask.state).toEqual(State.Rejected);
     });
   });
+
+  describe('map', () => {
+    describe('with both arguments', () => {
+      test('for a pending promise', async () => {
+        let { task, resolve } = Task.withResolvers<number, string>();
+        let theTask = map((n: number) => n % 2 == 0, task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<boolean, string>>();
+
+        resolve(123);
+        await theTask;
+      });
+
+      test('when the promise resolves', async () => {
+        let { task, resolve } = Task.withResolvers<number, string>();
+        let theTask = map((n: number) => n % 2 == 0, task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<boolean, string>>();
+
+        resolve(123);
+        let result = await theTask;
+        expect(unwrap(result)).toBe(false);
+      });
+
+      test('when the promise rejects', async () => {
+        let { task, reject } = Task.withResolvers<number, string>();
+        let theTask = map((n: number) => n % 2 == 0, task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<boolean, string>>();
+
+        let theReason = 'nope';
+        reject(theReason);
+        let result = await theTask;
+        expect(unwrapErr(result)).toEqual(theReason);
+      });
+    });
+
+    describe('with curried form', () => {
+      test('for a pending promise', async () => {
+        let { task, resolve } = Task.withResolvers<number, string>();
+        let theTask = map((n: number) => n % 2 == 0)(task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<boolean, unknown>>();
+
+        resolve(123);
+        await theTask;
+      });
+
+      test('when the promise resolves', async () => {
+        let { task, resolve } = Task.withResolvers<number, string>();
+        let theTask = map((n: number) => n % 2 == 0)(task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<boolean, unknown>>();
+
+        resolve(123);
+        let result = await theTask;
+        expect(unwrap(result)).toBe(false);
+      });
+
+      test('when the promise rejects', async () => {
+        let { task, reject } = Task.withResolvers<number, string>();
+        let theTask = map((n: number) => n % 2 == 0)(task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<boolean, unknown>>();
+
+        let theReason = 'nope';
+        reject(theReason);
+        let result = await theTask;
+        expect(unwrapErr(result)).toEqual(theReason);
+      });
+    });
+  });
+
+  describe('mapRejected', () => {
+    describe('with both arguments', () => {
+      test('for a pending promise', async () => {
+        let { task } = Task.withResolvers<number, string>();
+        let theTask = mapRejected(stringify, task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<number, string>>();
+      });
+
+      test('when the promise resolves', async () => {
+        let { task, resolve } = Task.withResolvers<number, string>();
+        let theTask = mapRejected(stringify, task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<number, string>>();
+
+        resolve(123);
+        let result = await theTask;
+        expect(unwrap(result)).toBe(123);
+      });
+
+      test('when the promise rejects', async () => {
+        let { task, reject } = Task.withResolvers<number, string>();
+        let theTask = mapRejected(stringify, task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<number, string>>();
+
+        let theReason = 'nope';
+        reject(theReason);
+        let result = await theTask;
+        expect(unwrapErr(result)).toEqual(stringify(theReason));
+      });
+    });
+
+    describe('with curried form', () => {
+      test('for a pending promise', async () => {
+        let { task } = Task.withResolvers<number, string>();
+        let theTask = mapRejected(stringify)(task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<unknown, string>>();
+      });
+
+      test('when the promise resolves', async () => {
+        let { task, resolve } = Task.withResolvers<number, string>();
+        let theTask = mapRejected(stringify)(task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<unknown, string>>();
+
+        resolve(123);
+        let result = await theTask;
+        expect(unwrap(result)).toBe(123);
+      });
+
+      test('when the promise rejects', async () => {
+        let { task, reject } = Task.withResolvers<number, string>();
+        let theTask = mapRejected(stringify)(task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<unknown, string>>();
+
+        let theReason = 'nope';
+        reject(theReason);
+        let result = await theTask;
+        expect(unwrapErr(result)).toEqual(stringify(theReason));
+      });
+    });
+  });
+
+  describe('and', () => {
+    describe('with both arguments', () => {
+      describe('when the first Task resolves', () => {
+        test('when the second Task resolves', async () => {
+          let theValue = 'hello';
+          let theTask = and(Task.resolve(theValue), Task.resolve(123));
+          expectTypeOf(theTask).toEqualTypeOf<Task<string, never>>();
+          let theResult = await theTask;
+          expect(unwrap(theResult)).toEqual(theValue);
+        });
+
+        test('when the second Task rejects', async () => {
+          let theReason = 'hello';
+          let theTask = and(
+            Task.reject<number, string>(theReason),
+            Task.resolve<number, string>(123)
+          );
+          expectTypeOf(theTask).toEqualTypeOf<Task<number, string>>();
+          let theResult = await theTask;
+          expect(unwrapErr(theResult)).toEqual(theReason);
+        });
+      });
+
+      describe('when the first Task rejects', () => {
+        test('when the second Task resolves', async () => {
+          let theReason = 123;
+          let theTask = and(
+            Task.resolve<number, number>(456),
+            Task.reject<string, number>(theReason)
+          );
+          expectTypeOf(theTask).toEqualTypeOf<Task<number, number>>();
+          let theResult = await theTask;
+          expect(unwrapErr(theResult)).toEqual(theReason);
+        });
+
+        test('when the second Task rejects', async () => {
+          let theReason = 123;
+          let theTask = and(
+            Task.reject<string, number>(456),
+            Task.reject<string, number>(theReason)
+          );
+          expectTypeOf(theTask).toEqualTypeOf<Task<string, number>>();
+          let theResult = await theTask;
+          expect(unwrapErr(theResult)).toEqual(theReason);
+        });
+      });
+    });
+
+    describe('with curried form', () => {
+      describe('when the first Task resolves', () => {
+        test('when the second Task resolves', async () => {
+          let theValue = 'hello';
+          let theTask = and(Task.resolve(theValue))(Task.resolve(123));
+          expectTypeOf(theTask).toEqualTypeOf<Task<string, never>>();
+          let theResult = await theTask;
+          expect(unwrap(theResult)).toEqual(theValue);
+        });
+
+        test('when the second Task rejects', async () => {
+          let theReason = 'hello';
+          let theTask = and(Task.reject<number, string>(theReason))(
+            Task.resolve<number, string>(123)
+          );
+          expectTypeOf(theTask).toEqualTypeOf<Task<number, string>>();
+          let theResult = await theTask;
+          expect(unwrapErr(theResult)).toEqual(theReason);
+        });
+      });
+
+      describe('when the first Task rejects', () => {
+        test('when the second Task resolves', async () => {
+          let theReason = 123;
+          let theTask = and(Task.resolve<number, number>(456))(
+            Task.reject<string, number>(theReason)
+          );
+          expectTypeOf(theTask).toEqualTypeOf<Task<number, number>>();
+          let theResult = await theTask;
+          expect(unwrapErr(theResult)).toEqual(theReason);
+        });
+
+        test('when the second Task rejects', async () => {
+          let theReason = 123;
+          let theTask = and(Task.reject<string, number>(456))(
+            Task.reject<string, number>(theReason)
+          );
+          expectTypeOf(theTask).toEqualTypeOf<Task<string, number>>();
+          let theResult = await theTask;
+          expect(unwrapErr(theResult)).toEqual(theReason);
+        });
+      });
+    });
+  });
+
+  describe('andThen', () => {
+    describe('with both arguments', () => {
+      test('for a pending task', async () => {
+        let { task, resolve } = Task.withResolvers<number, string>();
+        let theTask = andThen((n) => Task.resolve(n % 2 == 0), task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<boolean, string>>();
+
+        resolve(123);
+        await theTask;
+      });
+
+      test('when the task resolves', async () => {
+        let { task, resolve } = Task.withResolvers<number, string>();
+        let theTask = andThen((n) => Task.resolve(n % 2 == 0), task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<boolean, string>>();
+
+        resolve(123);
+        let result = await theTask;
+        expect(unwrap(result)).toBe(false);
+      });
+
+      test('when the task rejects', async () => {
+        let { task, reject } = Task.withResolvers<number, string>();
+        let theTask = andThen(() => Task.reject('oh no'), task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<never, string>>();
+
+        let theReason = 'nope';
+        reject(theReason);
+        let result = await theTask;
+        expect(unwrapErr(result)).toEqual(theReason);
+      });
+    });
+
+    describe('with curried form', () => {
+      test('for a pending task', async () => {
+        let { task, resolve } = Task.withResolvers<number, string>();
+        let theTask = andThen((n: number) => Task.resolve(n % 2 == 0))(task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<boolean, unknown>>();
+
+        resolve(123);
+        await theTask;
+      });
+
+      test('when the task resolves', async () => {
+        let { task, resolve } = Task.withResolvers<number, string>();
+        let theTask = andThen((n: number) => Task.resolve(n % 2 == 0))(task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<boolean, unknown>>();
+
+        resolve(123);
+        let result = await theTask;
+        expect(unwrap(result)).toBe(false);
+      });
+
+      test('when the task rejects', async () => {
+        let { task, reject } = Task.withResolvers<number, string>();
+        let theTask = andThen(() => Task.reject('oh no'))(task);
+        expectTypeOf(theTask).toEqualTypeOf<Task<never, unknown>>();
+
+        let theReason = 'nope';
+        reject(theReason);
+        let result = await theTask;
+        expect(unwrapErr(result)).toEqual(theReason);
+      });
+    });
+  });
+
+  describe('or', () => {
+    describe('with both arguments', () => {
+      describe('when the first Task resolves', () => {
+        test('when the second Task resolves', async () => {
+          let theTask = or(Task.resolve('B'), Task.resolve('A'));
+          expectTypeOf(theTask).toEqualTypeOf<Task<string, never>>();
+          let theResult = await theTask;
+          expect(unwrap(theResult)).toEqual('A');
+        });
+
+        test('when the second Task rejects', async () => {
+          let theReason = 'hello';
+          let theTask = or(
+            Task.reject<number, string>(theReason),
+            Task.resolve<number, string>(123)
+          );
+          expectTypeOf(theTask).toEqualTypeOf<Task<number, string>>();
+          let theResult = await theTask;
+          expect(unwrap(theResult)).toBe(123);
+        });
+      });
+
+      describe('when the first Task rejects', () => {
+        test('when the second Task resolves', async () => {
+          let theTask = or(Task.resolve('B'), Task.reject<string, number>(123));
+          expectTypeOf(theTask).toEqualTypeOf<Task<string, never>>();
+          let theResult = await theTask;
+          expect(unwrap(theResult)).toBe('B');
+        });
+
+        test('when the second Task rejects', async () => {
+          let theTask = or(Task.reject<string, number>(456), Task.reject<string, number>(123));
+          expectTypeOf(theTask).toEqualTypeOf<Task<string, number>>();
+          let theResult = await theTask;
+          expect(unwrapErr(theResult)).toBe(456);
+        });
+      });
+    });
+
+    describe('with curried form', () => {
+      describe('when the first Task resolves', () => {
+        test('when the second Task resolves', async () => {
+          let theTask = or(Task.resolve('B'))(Task.resolve('A'));
+          expectTypeOf(theTask).toEqualTypeOf<Task<unknown, never>>();
+          let theResult = await theTask;
+          expect(unwrap(theResult)).toEqual('A');
+        });
+
+        test('when the second Task rejects', async () => {
+          let theReason = 'hello';
+          let theTask = or(Task.reject<number, string>(theReason))(
+            Task.resolve<number, string>(123)
+          );
+          expectTypeOf(theTask).toEqualTypeOf<Task<unknown, string>>();
+          let theResult = await theTask;
+          expect(unwrap(theResult)).toBe(123);
+        });
+      });
+
+      describe('when the first Task rejects', () => {
+        test('when the second Task resolves', async () => {
+          let theTask = or(Task.resolve('B'))(Task.reject<string, number>(123));
+          expectTypeOf(theTask).toEqualTypeOf<Task<unknown, never>>();
+          let theResult = await theTask;
+          expect(unwrap(theResult)).toBe('B');
+        });
+
+        test('when the second Task rejects', async () => {
+          let theTask = or(Task.reject<string, number>(456))(Task.reject<string, number>(123));
+          expectTypeOf(theTask).toEqualTypeOf<Task<unknown, number>>();
+          let theResult = await theTask;
+          expect(unwrapErr(theResult)).toBe(456);
+        });
+      });
+    });
+  });
+
+  describe('orElse', () => {
+    test('for a pending promise', async () => {
+      let theTask = orElse((reason) => Task.resolve(reason.length), new Task<number, string>(noOp));
+      expectTypeOf(theTask).toEqualTypeOf<Task<number, never>>();
+
+      expect(theTask.state).toBe(State.Pending);
+    });
+
+    test('when the promise resolves', async () => {
+      let theTask = orElse(
+        (reason) => Task.resolve(reason.length),
+        Task.resolve<number, string>(123)
+      );
+      expectTypeOf(theTask).toEqualTypeOf<Task<number, never>>();
+
+      let result = await theTask;
+      expect(unwrap(result)).toBe(123);
+    });
+
+    test('when the promise rejects', async () => {
+      let theTask = orElse(
+        (reason) => Task.reject(reason.length),
+        Task.reject<number, string>('first error')
+      );
+      expectTypeOf(theTask).toEqualTypeOf<Task<number, number>>();
+
+      let result = await theTask;
+      expect(unwrapErr(result)).toBe(11);
+    });
+  });
+
+  describe('match', () => {
+    describe('with both arguments', () => {
+      test('with a resolved task', async () => {
+        let result = await match(
+          {
+            Resolved: (n) => n * 2,
+            Rejected: () => -1,
+          },
+          Task.resolve(2)
+        );
+        expect(result).toBe(4);
+      });
+
+      test('with a rejected task', async () => {
+        let result = await match(
+          {
+            Resolved: (n) => n * 2,
+            Rejected: () => -1,
+          },
+          Task.reject('oops')
+        );
+        expect(result).toBe(-1);
+      });
+
+      test('with a pending task', async () => {
+        let result = match(
+          {
+            Resolved: (n: number) => n * 2,
+            Rejected: () => -1,
+          },
+          new Task(noOp)
+        );
+        expectTypeOf(result).toEqualTypeOf<Promise<number>>();
+      });
+    });
+
+    describe('with curried form', () => {
+      test('with a resolved task', async () => {
+        let result = await match({
+          Resolved: (n: number) => n * 2,
+          Rejected: () => -1,
+        })(Task.resolve(2));
+        expect(result).toBe(4);
+      });
+
+      test('with a rejected task', async () => {
+        let result = await match({
+          Resolved: (n: number) => n * 2,
+          Rejected: () => -1,
+        })(Task.reject('oops'));
+        expect(result).toBe(-1);
+      });
+
+      test('with a pending task', async () => {
+        let result = match({
+          Resolved: (n: number) => n * 2,
+          Rejected: () => -1,
+        })(new Task(noOp));
+        expectTypeOf(result).toEqualTypeOf<Promise<number>>();
+      });
+    });
+  });
+
+  describe('timeout', () => {
+    describe('with both arguments', () => {
+      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 timeout(1, task);
+          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));
+          let result = await timeout(duration, 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));
+          let result = await timeout(duration * 2, 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 timeout(timer(1), task);
+          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));
+          let result = await timeout(timer(duration), 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));
+          let result = await timeout(timer(duration * 2), task);
+          expect(result.isOk).toBe(true);
+          expect(unwrap(result)).toBe(duration);
+        });
+      });
+    });
+
+    describe('with curried form', () => {
+      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 timeout(1)(task);
+          expect(result.isErr).toBe(true);
+          if (result.isErr) {
+            expect((result.error as Timeout).duration).toBe(1);
+          } else {
+            expect.unreachable();
+          }
+        });
+
+        test('that is equal', async () => {
+          let duration = 1;
+          let task = new Task((resolve) => setTimeout(() => resolve(duration), duration));
+          let result = await timeout(duration)(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));
+          let result = await timeout(duration * 2)(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 timeout(timer(1))(task);
+          expect(result.isErr).toBe(true);
+          if (result.isErr) {
+            expect((result.error as Timeout).duration).toBe(1);
+          } else {
+            expect.unreachable();
+          }
+        });
+
+        test('that is equal', async () => {
+          let duration = 1;
+          let task = new Task((resolve) => setTimeout(() => resolve(duration), duration));
+          let result = await timeout(timer(duration))(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));
+          let result = await timeout(timer(duration * 2))(task);
+          expect(result.isOk).toBe(true);
+          expect(unwrap(result)).toBe(duration);
+        });
+      });
+    });
+  });
+
+  describe('toPromise', () => {
+    test('with a directly-constructed task', async () => {
+      let { task, resolve } = Task.withResolvers();
+      let promise = toPromise(task);
+
+      let theValue = 'hello';
+      resolve(theValue);
+      let output = await promise;
+      expect(unwrap(output)).toEqual(theValue);
+    });
+
+    test('with a passed-in-promise', async () => {
+      let { promise: theInputPromise, resolve } = deferred();
+      let theTask = fromPromise(theInputPromise);
+
+      let theValue = 123;
+      resolve(theValue);
+      let theResult = await toPromise(theTask);
+      expect(unwrap(theResult)).toEqual(theValue);
+    });
+  });
 });
 
 describe('type utilities', () => {