diff --git a/.helix/languages.toml b/.helix/languages.toml
new file mode 100644
index 0000000..7694e46
--- /dev/null
+++ b/.helix/languages.toml
@@ -0,0 +1,49 @@
+# Project-local Helix config for phaseshift.
+#
+# Attaches per-language tooling:
+# - typescript-language-server: types + completion (LSP)
+# - oxlint --lsp: lint diagnostics (LSP)
+# - oxfmt --stdin-filepath: formatting on save (external formatter)
+#
+# oxfmt is wired as an external CLI formatter rather than an LSP because
+# its --lsp mode doesn't currently advertise textDocument/formatting, so
+# Helix's auto-format silently falls through to tsserver's built-in (which
+# isn't what we want). The CLI mode reads stdin, writes stdout, and uses
+# --stdin-filepath to infer the language.
+#
+# Both oxlint and oxfmt are launched via `pnpm exec` so they use the
+# repo-pinned versions in node_modules/.bin.
+
+[[language]]
+name = "typescript"
+language-servers = ["typescript-language-server", "oxlint"]
+auto-format = true
+formatter = { command = "pnpm", args = ["exec", "oxfmt", "--stdin-filepath=stdin.ts"] }
+
+[[language]]
+name = "tsx"
+language-servers = ["typescript-language-server", "oxlint"]
+auto-format = true
+formatter = { command = "pnpm", args = ["exec", "oxfmt", "--stdin-filepath=stdin.tsx"] }
+
+[[language]]
+name = "javascript"
+language-servers = ["typescript-language-server", "oxlint"]
+auto-format = true
+formatter = { command = "pnpm", args = ["exec", "oxfmt", "--stdin-filepath=stdin.js"] }
+
+[[language]]
+name = "jsx"
+language-servers = ["typescript-language-server", "oxlint"]
+auto-format = true
+formatter = { command = "pnpm", args = ["exec", "oxfmt", "--stdin-filepath=stdin.jsx"] }
+
+# Pin tsserver to the project's typescript install. Defensive fix
+# for the resolution issue we hit earlier — keeps Helix's LSP using
+# the exact same TS as `pnpm build`.
+[language-server.typescript-language-server.config]
+typescript.tsdk = "node_modules/typescript/lib"
+
+[language-server.oxlint]
+command = "pnpm"
+args = ["exec", "oxlint", "--lsp"]
diff --git a/LICENSE-APACHE.md b/LICENSE-APACHE.md
index d0af96c..5312f82 100644
--- a/LICENSE-APACHE.md
+++ b/LICENSE-APACHE.md
@@ -1,5 +1,4 @@
-Apache License
-==============
+# Apache License
_Version 2.0, January 2004_
_< >_
@@ -89,27 +88,27 @@ You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
-* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
-this License; and
-* **(b)** You must cause any modified files to carry prominent notices stating that You
-changed the files; and
-* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
-all copyright, patent, trademark, and attribution notices from the Source form
-of the Work, excluding those notices that do not pertain to any part of the
-Derivative Works; and
-* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
-Derivative Works that You distribute must include a readable copy of the
-attribution notices contained within such NOTICE file, excluding those notices
-that do not pertain to any part of the Derivative Works, in at least one of the
-following places: within a NOTICE text file distributed as part of the
-Derivative Works; within the Source form or documentation, if provided along
-with the Derivative Works; or, within a display generated by the Derivative
-Works, if and wherever such third-party notices normally appear. The contents of
-the NOTICE file are for informational purposes only and do not modify the
-License. You may add Your own attribution notices within Derivative Works that
-You distribute, alongside or as an addendum to the NOTICE text from the Work,
-provided that such additional attribution notices cannot be construed as
-modifying the License.
+- **(a)** You must give any other recipients of the Work or Derivative Works a copy of
+ this License; and
+- **(b)** You must cause any modified files to carry prominent notices stating that You
+ changed the files; and
+- **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
+ all copyright, patent, trademark, and attribution notices from the Source form
+ of the Work, excluding those notices that do not pertain to any part of the
+ Derivative Works; and
+- **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
+ Derivative Works that You distribute must include a readable copy of the
+ attribution notices contained within such NOTICE file, excluding those notices
+ that do not pertain to any part of the Derivative Works, in at least one of the
+ following places: within a NOTICE text file distributed as part of the
+ Derivative Works; within the Source form or documentation, if provided along
+ with the Derivative Works; or, within a display generated by the Derivative
+ Works, if and wherever such third-party notices normally appear. The contents of
+ the NOTICE file are for informational purposes only and do not modify the
+ License. You may add Your own attribution notices within Derivative Works that
+ You distribute, alongside or as an addendum to the NOTICE text from the Work,
+ provided that such additional attribution notices cannot be construed as
+ modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
@@ -180,13 +179,13 @@ the same “printed page” as the copyright notice for easier identification wi
third-party archives.
Copyright [yyyy] [name of copyright owner]
-
+
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
-
+
http://www.apache.org/licenses/LICENSE-2.0
-
+
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
diff --git a/package.json b/package.json
index acdcdf2..b853a73 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,8 @@
"devDependencies": {
"oxfmt": "^0.55.0",
"oxlint": "^1.70.0",
- "turbo": "latest"
+ "turbo": "latest",
+ "typescript": "^6.0.3"
},
"packageManager": "pnpm@10.29.2"
-}
\ No newline at end of file
+}
diff --git a/packages/core/README.md b/packages/core/README.md
index fcea24b..9b01741 100644
--- a/packages/core/README.md
+++ b/packages/core/README.md
@@ -1 +1 @@
-# `@phaseshift/core`
\ No newline at end of file
+# `@phaseshift/core`
diff --git a/packages/core/package.json b/packages/core/package.json
index a3b1059..96c2a78 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,13 +1,13 @@
{
"name": "@phaseshift/core",
- "sideEffects": false,
"version": "0.0.0",
"private": true,
- "types": "./dist/index.d.ts",
- "main": "./dist/index.js",
"files": [
"dist"
],
+ "sideEffects": false,
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
@@ -23,7 +23,6 @@
},
"devDependencies": {
"@phaseshift/tsconfig": "workspace:*",
- "typescript": "^6.0.3",
"vitest": "^4.1.9"
}
-}
\ No newline at end of file
+}
diff --git a/packages/core/src/option.test.ts b/packages/core/src/option.test.ts
new file mode 100644
index 0000000..bd529b8
--- /dev/null
+++ b/packages/core/src/option.test.ts
@@ -0,0 +1,325 @@
+import { describe, expect, expectTypeOf, it } from "vitest";
+import { None, Option, Some } from "./option";
+
+describe("Option", () => {
+ describe("runtime", () => {
+ it("Some carries a value, None doesn't", () => {
+ const s = Some(42);
+ const n = None();
+ expect(s.isSome()).toBe(true);
+ expect(n.isSome()).toBe(false);
+ if (s.isSome()) expect(s.value).toBe(42);
+ });
+
+ // None() returns the same internal instance every call — the function
+ // wrapper exists for the `None(): Option` calling convention,
+ // but there's only ever one None object at runtime.
+ it("None() returns a shared internal instance", () => {
+ const a = None();
+ const b = None();
+ expect(a).toBe(b);
+ expect(Option.None()).toBe(None());
+ });
+
+ // isSomeAnd intentionally returns a plain boolean — it never narrows
+ // `this`, regardless of whether the lambda is an explicit type guard,
+ // an implicit type-predicate pattern, or a plain boolean expression.
+ // TS 5.5+'s inferred type predicates don't fire for `this is X` return
+ // types, so a narrowing variant would only work with explicit
+ // annotations. We chose the consistent always-boolean shape and put
+ // narrowing on `filter` instead (see below).
+ it("isSomeAnd: returns plain boolean without narrowing", () => {
+ const x: Option = Some("hi");
+
+ // Explicit type-guard predicate: still doesn't narrow x.
+ if (x.isSomeAnd((v): v is string => typeof v === "string")) {
+ expectTypeOf().toEqualTypeOf>();
+ }
+
+ // Implicit type-predicate-shaped lambda: same.
+ if (x.isSomeAnd((v) => typeof v === "string")) {
+ expectTypeOf().toEqualTypeOf>();
+ }
+
+ // Plain predicate: returns the boolean result of the predicate.
+ const y: Option = Some(5);
+ expect(y.isSomeAnd((v) => 3 < v)).toBe(true);
+ expect(y.isSomeAnd((v) => 10 < v)).toBe(false);
+ expect(None().isSomeAnd((v) => 3 < v)).toBe(false);
+ expectTypeOf().toEqualTypeOf>();
+ });
+
+ // filter is the path to narrowing. Returns Option (a value, not a
+ // `this is X` predicate), so TS 5.5+'s inferred type predicates fire
+ // and the implicit lambda gets treated as a type guard.
+ it("filter: narrows via TS 5.5+ inferred type predicates", () => {
+ const x: Option = Some("hi");
+ const filtered = x.filter((v) => typeof v === "string");
+ expectTypeOf().toEqualTypeOf>();
+ if (filtered.isSome()) {
+ expectTypeOf(filtered.value).toEqualTypeOf();
+ expect(filtered.value).toBe("hi");
+ }
+ });
+
+ it("filter: returns None when predicate fails", () => {
+ const x: Option = Some(2);
+ const big = x.filter((v) => 10 < v);
+ expect(big.isNone()).toBe(true);
+ });
+
+ it("filter: returns None when called on None", () => {
+ const x = None();
+ const filtered = x.filter((v) => 0 < v.length);
+ expect(filtered.isNone()).toBe(true);
+ });
+
+ it("Option.Some / Option.None namespace", () => {
+ const a = Option.Some(1);
+ const b = Option.None();
+ expect(a.isSome()).toBe(true);
+ expect(b.isNone()).toBe(true);
+ });
+
+ it("Option(x) callable form wraps nullable values", () => {
+ expect(Option(42).isSome()).toBe(true);
+ expect(Option(0).isSome()).toBe(true); // 0 is a valid Some value
+ expect(Option("").isSome()).toBe(true); // empty string is a valid Some value
+ expect(Option(false).isSome()).toBe(true); // false is a valid Some value
+ expect(Option(null).isNone()).toBe(true);
+ expect(Option(undefined).isNone()).toBe(true);
+ });
+
+ it("Option.from(x) is an alias for Option(x)", () => {
+ expect(Option.from(42)).toEqual(Option(42));
+ expect(Option.from(null)).toEqual(Option(null));
+ expect(Option.from(undefined)).toEqual(Option(undefined));
+ });
+
+ it("Option(x) returns Some value preserved", () => {
+ const opt = Option("hello");
+ if (opt.isSome()) {
+ expect(opt.value).toBe("hello");
+ } else {
+ throw new Error("expected Some");
+ }
+ });
+ });
+
+ describe("method behavior", () => {
+ it("isNone: true for None, false for Some", () => {
+ expect(Some(2).isNone()).toBe(false);
+ expect(None().isNone()).toBe(true);
+ });
+
+ it("isNoneOr: None returns true, Some checks predicate", () => {
+ const x: Option = Some(2);
+ expect(x.isNoneOr((v) => 1 < v)).toBe(true);
+ const y: Option = Some(0);
+ expect(y.isNoneOr((v) => 1 < v)).toBe(false);
+ const z: Option = None();
+ expect(z.isNoneOr((v) => 1 < v)).toBe(true);
+ });
+
+ it("expect: returns value on Some, throws with message on None", () => {
+ const x = Some("value");
+ expect(x.expect("fruits are healthy")).toBe("value");
+
+ const y = None();
+ expect(() => y.expect("fruits are healthy")).toThrow("fruits are healthy");
+ });
+
+ it("unwrap: returns value on Some, throws on None", () => {
+ const x = Some("air");
+ expect(x.unwrap()).toBe("air");
+
+ const y = None();
+ expect(() => y.unwrap()).toThrow();
+ });
+
+ it("unwrapOr: returns value on Some, fallback on None", () => {
+ expect(Some("car").unwrapOr("bike")).toBe("car");
+ expect(None().unwrapOr("bike")).toBe("bike");
+ });
+
+ it("unwrapOrElse: returns value on Some, computed fallback on None", () => {
+ const k = 10;
+ expect(Some(4).unwrapOrElse(() => 2 * k)).toBe(4);
+ expect(None().unwrapOrElse(() => 2 * k)).toBe(20);
+ });
+
+ it("map: applies fn to Some value, leaves None unchanged", () => {
+ const maybeSomeString = Some("Hello, world!");
+ const maybeSomeLength = maybeSomeString.map((s) => s.length);
+ expect(maybeSomeLength).toEqual(Some(13));
+
+ const x: Option = None();
+ expect(x.map((s) => s.length)).toEqual(None());
+ });
+
+ it("inspect: runs side effect on Some, returns original option", () => {
+ const seen: number[] = [];
+ const x = Some(42).inspect((v) => seen.push(v));
+ expect(seen).toEqual([42]);
+ expect(x).toEqual(Some(42));
+
+ const y = None().inspect((v) => seen.push(v));
+ expect(seen).toEqual([42]); // unchanged — None doesn't invoke
+ expect(y.isNone()).toBe(true);
+ });
+
+ it("mapOr: applies fn to Some, returns fallback for None", () => {
+ const x = Some("foo");
+ expect(x.mapOr(42, (v) => v.length)).toBe(3);
+
+ const y: Option = None();
+ expect(y.mapOr(42, (v) => v.length)).toBe(42);
+ });
+
+ it("mapOrElse: applies fn to Some, computes fallback for None", () => {
+ const k = 21;
+ const x = Some("foo");
+ expect(
+ x.mapOrElse(
+ () => 2 * k,
+ (v) => v.length,
+ ),
+ ).toBe(3);
+
+ const y: Option = None();
+ expect(
+ y.mapOrElse(
+ () => 2 * k,
+ (v) => v.length,
+ ),
+ ).toBe(42);
+ });
+
+ it("and: None.and(_) is None, Some.and(other) is other", () => {
+ const x: Option = Some(2);
+ const yNone: Option = None();
+ expect(x.and(yNone)).toEqual(None());
+
+ const noneN: Option = None();
+ const ySome: Option = Some("foo");
+ expect(noneN.and(ySome)).toEqual(None());
+
+ expect(Some(2).and(Some("foo"))).toEqual(Some("foo"));
+ expect(None().and(None())).toEqual(None());
+ });
+
+ it("andThen: None.andThen(_) is None, Some chains the fn", () => {
+ const sqThenToString = (x: number): Option =>
+ 0 <= x ? Some(Math.sqrt(x).toString()) : None();
+
+ expect(Some(4).andThen(sqThenToString)).toEqual(Some("2"));
+ expect(Some(-1).andThen(sqThenToString)).toEqual(None());
+ expect(None().andThen(sqThenToString)).toEqual(None());
+ });
+
+ it("or: Some takes precedence over either operand", () => {
+ const x: Option = Some(2);
+ const y: Option = None();
+ expect(x.or(y)).toEqual(Some(2));
+
+ const a: Option = None();
+ const b: Option = Some(100);
+ expect(a.or(b)).toEqual(Some(100));
+
+ expect(Some(2).or(Some(100))).toEqual(Some(2));
+ expect(None().or(None())).toEqual(None());
+ });
+
+ it("orElse: Some takes precedence, fallback is lazy", () => {
+ const nobody = (): Option => None();
+ const vikings = (): Option => Some("vikings");
+
+ expect(Some("barbarians").orElse(vikings)).toEqual(Some("barbarians"));
+ expect(None().orElse(vikings)).toEqual(Some("vikings"));
+ expect(None().orElse(nobody)).toEqual(None());
+ });
+
+ it("xor: Some iff exactly one operand is Some", () => {
+ expect(Some(2).xor(None())).toEqual(Some(2));
+ expect(None().xor(Some(2))).toEqual(Some(2));
+ expect(Some(2).xor(Some(2))).toEqual(None());
+ expect(None().xor(None())).toEqual(None());
+ });
+
+ it("flatten: unwraps one level of Option nesting", () => {
+ const x: Option> = Some(Some(6));
+ expect(x.flatten()).toEqual(Some(6));
+
+ const y: Option > = Some(None());
+ expect(y.flatten()).toEqual(None());
+
+ const z: Option > = None();
+ expect(z.flatten()).toEqual(None());
+ });
+
+ it("flatten: only removes one level at a time", () => {
+ const x: Option >> = Some(Some(Some(6)));
+ expect(x.flatten()).toEqual(Some(Some(6)));
+ expect(x.flatten().flatten()).toEqual(Some(6));
+ });
+
+ it("zip: Some+Some produces tuple, any None produces None", () => {
+ const x = Some(1);
+ const y = Some("hi");
+ const z = None();
+
+ expect(x.zip(y)).toEqual(Some([1, "hi"]));
+ expect(x.zip(z)).toEqual(None());
+ expect(z.zip(x)).toEqual(None());
+ expect(z.zip(z)).toEqual(None());
+ });
+
+ it("zipWith: applies fn to two Somes, any None produces None", () => {
+ type Point = { x: number; y: number };
+ const newPoint = (x: number, y: number): Point => ({ x, y });
+
+ const xCoord = Some(17.5);
+ const yCoord = Some(42.7);
+
+ expect(xCoord.zipWith(yCoord, newPoint)).toEqual(Some({ x: 17.5, y: 42.7 }));
+ expect(xCoord.zipWith(None(), newPoint)).toEqual(None());
+ expect(None().zipWith(yCoord, newPoint)).toEqual(None());
+ });
+
+ it("unzip: Some tuple splits into tuple of Somes, None splits into [None, None]", () => {
+ const x = Some<[number, string]>([1, "hi"]);
+ expect(x.unzip()).toEqual([Some(1), Some("hi")]);
+
+ const y: Option<[number, string]> = None();
+ expect(y.unzip()).toEqual([None(), None()]);
+ });
+
+ it("Symbol.iterator: Some yields its value once, None yields nothing", () => {
+ const yielded: number[] = [];
+ for (const v of Some(4)) {
+ yielded.push(v);
+ }
+ expect(yielded).toEqual([4]);
+
+ const noneYielded: number[] = [];
+ for (const v of None()) {
+ noneYielded.push(v);
+ }
+ expect(noneYielded).toEqual([]);
+ });
+
+ it("Symbol.iterator: works with spread and Array.from", () => {
+ expect([...Some(7)]).toEqual([7]);
+ expect([...None()]).toEqual([]);
+
+ expect(Array.from(Some("hi"))).toEqual(["hi"]);
+ expect(Array.from(None())).toEqual([]);
+ });
+
+ it("Symbol.iterator: each call returns a fresh iterator (not consumed)", () => {
+ const x = Some(5);
+ expect([...x]).toEqual([5]);
+ expect([...x]).toEqual([5]); // still works the second time
+ });
+ });
+});
diff --git a/packages/core/src/option.ts b/packages/core/src/option.ts
new file mode 100644
index 0000000..c4a4f8d
--- /dev/null
+++ b/packages/core/src/option.ts
@@ -0,0 +1,1089 @@
+/**
+ * Optional values.
+ *
+ * Type `Option` represents an optional value: every `Option` is either
+ * `Some` and contains a value, or `None`, and does not. `Option`s have a
+ * number of uses:
+ *
+ * - Initial values
+ * - Return values for functions that are not defined over their entire input
+ * range (partial functions)
+ * - Return value for otherwise reporting simple errors, where `None` is
+ * returned on error
+ * - Optional struct fields
+ * - Optional function arguments
+ * - Nullable pointers (browser/DOM APIs that return `T | null`)
+ * - Swapping things out of difficult situations
+ *
+ * `Option`s are commonly paired with narrowing to query the presence of a
+ * value and take action, always accounting for the `None` case:
+ *
+ * ```ts
+ * const divide = (numerator: number, denominator: number): Option =>
+ * denominator === 0 ? None() : Some(numerator / denominator);
+ *
+ * // The return value of the function is an Option
+ * const result = divide(2, 3);
+ *
+ * // Narrow to retrieve the value:
+ * if (result.isSome()) {
+ * console.log(`Result: ${result.value}`);
+ * } else {
+ * console.log("Cannot divide by 0");
+ * }
+ * ```
+ *
+ * # Options and nullable values
+ *
+ * JavaScript's primary nullable values are `null` and `undefined`. Rather
+ * than reasoning about which one a given API uses, wrap the call in `Option`
+ * and treat both as "no value":
+ *
+ * ```ts
+ * // localStorage.getItem returns `string | null`
+ * const theme = Option(localStorage.getItem("theme")).unwrapOr("light");
+ *
+ * // Array.prototype.find returns `T | undefined`
+ * const first = Option([1, 2, 3].find(n => 5 < n));
+ * ```
+ *
+ * # Method overview
+ *
+ * In addition to working with narrowing, `Option` provides a wide variety of
+ * different methods:
+ *
+ * ## Querying the variant
+ *
+ * The `isSome` and `isNone` methods return `true` if the `Option` is `Some`
+ * or `None`, respectively.
+ *
+ * The `isSomeAnd` and `isNoneOr` methods apply the provided function to the
+ * contents of the `Option` to produce a boolean value. If this is `None`
+ * then a default result is returned instead without executing the function.
+ *
+ * ## Extracting the contained value
+ *
+ * These methods extract the contained value in an `Option` when it is the
+ * `Some` variant. If the `Option` is `None`:
+ *
+ * - `expect` throws with a provided custom message
+ * - `unwrap` throws with a generic message
+ * - `unwrapOr` returns the provided default value
+ * - `unwrapOrElse` returns the result of evaluating the provided function
+ *
+ * ## Transforming contained values
+ *
+ * These methods transform the `Some` variant:
+ *
+ * - `filter` calls the provided predicate function on the contained value `t`
+ * if the `Option` is `Some(t)`, and returns `Some(t)` if the function
+ * returns `true`; otherwise, returns `None`
+ * - `flatten` removes one level of nesting from an `Option>`
+ * - `inspect` takes a function applied to the contained value if `Some`, and
+ * returns the original `Option`
+ * - `map` transforms `Option` to `Option` by applying the provided
+ * function to the contained value of `Some` and leaving `None` values
+ * unchanged
+ *
+ * These methods transform `Option` to a value of a possibly different
+ * type `U`:
+ *
+ * - `mapOr` applies the provided function to the contained value of `Some`,
+ * or returns the provided default value if the `Option` is `None`
+ * - `mapOrElse` applies the provided function to the contained value of
+ * `Some`, or returns the result of evaluating the provided fallback
+ * function if the `Option` is `None`
+ *
+ * These methods combine the `Some` variants of two `Option` values:
+ *
+ * - `zip` returns `Some([s, o])` if `self` is `Some(s)` and the provided
+ * `Option` value is `Some(o)`; otherwise, returns `None`
+ * - `zipWith` calls the provided function `f` and returns `Some(f(s, o))` if
+ * `self` is `Some(s)` and the provided `Option` value is `Some(o)`;
+ * otherwise, returns `None`
+ *
+ * ## Boolean operators
+ *
+ * These methods treat the `Option` as a boolean value, where `Some` acts
+ * like `true` and `None` acts like `false`.
+ *
+ * | method | self | input | output |
+ * | ----------- | --------- | ----------- | ----------- |
+ * | `and` | `None` | (ignored) | `None` |
+ * | `and` | `Some(x)` | `None` | `None` |
+ * | `and` | `Some(x)` | `Some(y)` | `Some(y)` |
+ * | `or` | `None` | `None` | `None` |
+ * | `or` | `None` | `Some(y)` | `Some(y)` |
+ * | `or` | `Some(x)` | (ignored) | `Some(x)` |
+ * | `xor` | `None` | `None` | `None` |
+ * | `xor` | `None` | `Some(y)` | `Some(y)` |
+ * | `xor` | `Some(x)` | `None` | `Some(x)` |
+ * | `xor` | `Some(x)` | `Some(y)` | `None` |
+ *
+ * The `andThen` and `orElse` methods take a function as input, and only
+ * evaluate the function when they need to produce a new value. Only the
+ * `andThen` method can produce an `Option` value having a different inner
+ * type `U` than `Option`.
+ *
+ * ## Iterating over `Option`
+ *
+ * An `Option` can be iterated over via `Symbol.iterator`. This can be
+ * helpful if you need an iterable that is conditionally empty. The iterator
+ * will either produce a single value (when the `Option` is `Some`), or
+ * produce no values (when the `Option` is `None`):
+ *
+ * ```ts
+ * const yep = Some(42);
+ * const nope = None();
+ *
+ * const nums = [0, 1, 2, 3, ...yep, 4, 5, 6, 7];
+ * // [0, 1, 2, 3, 42, 4, 5, 6, 7]
+ *
+ * const nums2 = [0, 1, 2, 3, ...nope, 4, 5, 6, 7];
+ * // [0, 1, 2, 3, 4, 5, 6, 7]
+ * ```
+ *
+ * # Immutability
+ *
+ * Phaseshift `Option`s are **immutable values**. Rust's mutating methods
+ * (`take`, `replace`, `insert`, `get_or_insert`, `get_or_insert_with`,
+ * `get_or_insert_default`, `take_if`) are intentionally absent — they rely
+ * on Rust's `&mut self` semantics, which don't translate cleanly to a
+ * dynamic language and would invite bugs when shared references are mutated
+ * unexpectedly.
+ *
+ * To replace the value held by a `let` binding, reassign the binding
+ * directly:
+ *
+ * ```ts
+ * let x: Option = Some(5);
+ *
+ * // Equivalent of Rust's `x.take()`:
+ * const taken = x;
+ * x = None();
+ *
+ * // Equivalent of Rust's `x.replace(10)`:
+ * const previous = x;
+ * x = Some(10);
+ * ```
+ *
+ * If a value needs to be conditionally substituted in a chain (Rust's
+ * `get_or_insert_with`), use `unwrapOrElse` followed by reassignment, or
+ * chain with `orElse`:
+ *
+ * ```ts
+ * let cache: Option = None();
+ * const value = cache.unwrapOrElse(() => "computed");
+ * cache = Some(value);
+ * ```
+ *
+ * @module
+ */
+
+type __Some = { __type: "Some"; value: T };
+type __None = { __type: "None" };
+
+/**
+ * The `Some` variant of `Option` — contains a value of type `T`.
+ *
+ * `isSome()` narrows an `Option` to `Some` in the truthy branch,
+ * making the contained `.value` available without further checks.
+ *
+ * Implements `Iterable`: iterating yields the contained value exactly
+ * once. See the module-level documentation for more.
+ *
+ * @template T The type of the contained value.
+ */
+export type Some = OptionMethods & __Some & Iterable;
+
+/**
+ * The `None` variant of `Option` — contains no value.
+ *
+ * `isNone()` narrows an `Option` to `None` in the truthy branch.
+ *
+ * Internally a singleton: every `None()` call returns the same instance,
+ * so `None() === None()` is `true`. Implements `Iterable`:
+ * iterating yields nothing. See the module-level documentation for more.
+ */
+export type None = OptionMethods & __None & Iterable;
+
+/**
+ * The set of methods available on every `Option` value.
+ *
+ * Used internally as the shared prototype for `Some` and `None`
+ * instances; exported because hovering a method on an `Option`
+ * surfaces these signatures.
+ *
+ * Methods are documented in Rust-faithful style (`# Examples`, `# Throws`,
+ * etc.). See the module-level documentation for a method overview.
+ */
+export type OptionMethods = {
+ /**
+ * Returns `true` if the option is a `Some` value.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const x: Option = Some(2);
+ * expect(x.isSome()).toEqual(true);
+ *
+ * const y: Option = None();
+ * expect(y.isSome()).toEqual(false);
+ * ```
+ *
+ * @template T The type of the option's value.
+ */
+ isSome(this: Option): this is Some;
+
+ /**
+ * Returns `true` if the option is a `Some` and the value inside of it
+ * matches a predicate.
+ *
+ * # Examples
+ * ```ts
+ * const x: Option = Some(2);
+ * expect(x.isSomeAnd((x) => 1 < x)).toEqual(true);
+ *
+ * const y: Option = Some(0);
+ * expect(y.isSomeAnd((x) => 1 < x)).toEqual(false);
+ *
+ * const z: Option = None();
+ * expect(z.isSomeAnd((x) => 1 < x)).toEqual(false);
+ * ```
+ *
+ * @template T The type of the option's value.
+ */
+ isSomeAnd(this: Option, f: (value: T) => boolean): boolean;
+
+ /**
+ * Returns `true` if the option is a `None` value.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const x: Option = Some(2);
+ * expect(x.isNone()).toEqual(false);
+ *
+ * const y: Option = None();
+ * expect(y.isNone()).toEqual(true);
+ * ```
+ *
+ * @template T The type of the option's value.
+ */
+ isNone(this: Option): this is None;
+
+ /**
+ * Returns `true` if the option is a `None` or the value inside of it
+ * matches a predicate.
+ *
+ * # Examples
+ * ```ts
+ * const x: Option = Some(2);
+ * expect(x.isNoneOr((x) => 1 < x)).toEqual(true);
+ *
+ * const y: Option = Some(0);
+ * expect(y.isNoneOr((x) => 1 < x)).toEqual(false);
+ *
+ * const z: Option = None();
+ * expect(z.isNoneOr((x) => 1 < x)).toEqual(true);
+ * ```
+ *
+ * @template T The type of the option's value.
+ */
+ isNoneOr(this: Option, f: (value: T) => boolean): boolean;
+
+ /**
+ * Returns the contained `Some` value.
+ *
+ * # Throws
+ *
+ * Throws if the option is a `None` value, with a custom error message
+ * provided by `message`.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const x = Some("value");
+ * expect(x.expect("fruits are healthy")).toEqual("value");
+ *
+ * const y = None();
+ * expect(() => y.expect("fruits are healthy"))
+ * .toThrow("fruits are healthy");
+ * ```
+ *
+ * # Recommended Message Style
+ *
+ * We recommend that `expect` messages are used to describe the reason you
+ * _expect_ the `Option` should be `Some`.
+ *
+ * ```ts
+ * const list = [1, 2, 3];
+ * const item = Option(list[0])
+ * .expect("list should not be empty");
+ * ```
+ *
+ * **Hint**: If you're having trouble remembering how to phrase expect error
+ * messages, remember to focus on the word "should" (as in "env variable
+ * should be set by blah" or "the given binary should be available and
+ * executable by the current user").
+ *
+ * @template T The type of the option's value.
+ */
+ expect(this: None, message: string): never;
+ expect(this: Option, message: string): T;
+
+ /**
+ * Returns the contained `Some` value.
+ *
+ * # Throws
+ *
+ * Throws if the option is a `None` value.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const x = Some("air");
+ * expect(x.unwrap()).toEqual("air");
+ *
+ * const y = None();
+ * expect(() => y.unwrap()).toThrow();
+ * ```
+ *
+ * @deprecated Because this function can throw exceptions, its use is
+ * generally discouraged. Exceptions are meant for unrecoverable situations,
+ * and may crash the entire page.
+ *
+ * Instead, prefer to use pattern matching and handle the `None` case
+ * explicitly, or call `unwrapOr` or `unwrapOrElse`.
+ */
+ unwrap(this: None): never;
+ /**
+ * Returns the contained `Some` value.
+ *
+ * # Throws
+ *
+ * Throws if the option is a `None` value.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const x = Some("air");
+ * expect(x.unwrap()).toEqual("air");
+ *
+ * const y = None();
+ * expect(() => y.unwrap()).toThrow();
+ * ```
+ *
+ * @deprecated Because this function can throw exceptions, its use is
+ * generally discouraged. Exceptions are meant for unrecoverable situations,
+ * and may crash the entire page.
+ *
+ * Instead, prefer to use pattern matching and handle the `None` case
+ * explicitly, or call `unwrapOr` or `unwrapOrElse`.
+ */
+ unwrap(this: Option): T;
+
+ /**
+ * Returns the contained `Some` value or a provided fallback.
+ *
+ * Arguments passed to `unwrapOr` are eagerly evaluated; if you are passing
+ * the result of a function call, it is recommended to use `unwrapOrElse`,
+ * which is lazily evaluated.
+ *
+ * # Examples
+ *
+ * ```ts
+ * expect(Some("car").unwrapOr("bike")).toEqual("car");
+ * expect(None().unwrapOr("bike")).toEqual("bike");
+ * ```
+ */
+ unwrapOr(this: Option, fallback: T): T;
+
+ /**
+ * Returns the contained `Some` value or computes it from a closure.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const k = 10;
+ * expect(Some(4).unwrapOrElse(() => 2 * k)).toEqual(4);
+ * expect(None().unwrapOrElse(() => 2 * k)).toEqual(20);
+ * ```
+ */
+ unwrapOrElse(this: Option, f: () => T): T;
+
+ /**
+ * Maps an `Option` to `Option` by applying a function to a contained
+ * value (if `Some`) or returns `None` (if `None`).
+ *
+ * # Examples
+ *
+ * Calculate the length of an `Option` as an `Option`:
+ *
+ * ```ts
+ * const maybeSomeString = Some("Hello, world!");
+ * const maybeSomeLength = maybeSomeString.map(s => s.length);
+ * expect(maybeSomeLength).toEqual(Some(13));
+ *
+ * const x: Option = None();
+ * expect(x.map(s => s.length)).toEqual(None());
+ * ```
+ */
+ map(this: Option, f: (value: T) => U): Option;
+
+ /**
+ * Calls a function with the contained value if `Some`.
+ *
+ * Returns the original option.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const list = [1, 2, 3];
+ *
+ * // prints "got: 2"
+ * const x = Option(list[1])
+ * .inspect(x => console.log(`got: ${x}`))
+ * .expect("list should be long enough");
+ *
+ * // prints nothing
+ * Option(list[5]).inspect(x => console.log(`got: ${x}`));
+ * ```
+ */
+ inspect(this: Option, fn: (value: T) => void): Option;
+
+ /**
+ * Returns the provided default result (if `None`), or applies a function to
+ * the contained value (if `Some`).
+ *
+ * Arguments passed to `mapOr` are eagerly evaluated; if you are passing the
+ * result of a function call, it is recommended to use `mapOrElse`, which is
+ * lazily evaluated.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const x = Some("foo");
+ * expect(x.mapOr(42, v => v.length)).toEqual(3);
+ *
+ * const y: Option = None();
+ * expect(y.mapOr(42, v => v.length)).toEqual(42);
+ * ```
+ */
+ mapOr(this: Option, fallback: U, fn: (value: T) => U): U;
+
+ /**
+ * Computes a fallback function result (if `None`), or applies a different
+ * function to the contained value (if `Some`).
+ *
+ * # Examples
+ *
+ * ```ts
+ * const k = 21;
+ *
+ * const x = Some("foo");
+ * expect(x.mapOrElse(() => 2 * k, v => v.length)).toEqual(3);
+ *
+ * const y: Option = None();
+ * expect(y.mapOrElse(() => 2 * k, v => v.length)).toEqual(42);
+ * ```
+ */
+ mapOrElse(this: Option, fallback: () => U, fn: (value: T) => U): U;
+
+ /**
+ * Returns `None` if the option is `None`, otherwise returns `other`.
+ *
+ * Arguments passed to `and` are eagerly evaluated; if you are passing the
+ * result of a function call, it is recommended to use `andThen`, which is
+ * lazily evaluated.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const x: Option = Some(2);
+ * const y: Option = None();
+ * expect(x.and(y)).toEqual(None());
+ *
+ * const a: Option = None();
+ * const b: Option = Some("foo");
+ * expect(a.and(b)).toEqual(None());
+ *
+ * const c: Option = Some(2);
+ * const d: Option = Some("foo");
+ * expect(c.and(d)).toEqual(Some("foo"));
+ *
+ * const e: Option = None();
+ * const f: Option = None();
+ * expect(e.and(f)).toEqual(None());
+ * ```
+ */
+ and(this: Option, other: Option): Option;
+
+ /**
+ * Returns `None` if the option is `None`, otherwise calls `fn` with the
+ * wrapped value and returns the result.
+ *
+ * Often used to chain fallible operations that may return `None`. Some
+ * languages call this operation "flatmap".
+ *
+ * # Examples
+ *
+ * ```ts
+ * const sqThenToString = (x: number): Option =>
+ * 0 <= x ? Some(Math.sqrt(x).toString()) : None();
+ *
+ * expect(Some(4).andThen(sqThenToString)).toEqual(Some("2"));
+ * expect(Some(-1).andThen(sqThenToString)).toEqual(None());
+ * expect(None().andThen(sqThenToString)).toEqual(None());
+ * ```
+ */
+ andThen(this: Option, fn: (value: T) => Option): Option;
+
+ /**
+ * Returns `None` if the option is `None`, otherwise calls `predicate` with
+ * the wrapped value and returns:
+ *
+ * - `Some(t)` if `predicate` returns `true` (where `t` is the wrapped value)
+ * - `None` if `predicate` returns `false`
+ *
+ * This function works similarly to `Array.prototype.filter`. You can imagine
+ * the `Option` being an iterator over one or zero elements: `filter`
+ * lets you decide which elements to keep.
+ *
+ * When `predicate` is a type guard (`(v: T) => v is U`), the returned
+ * `Option` carries the narrower value type — useful for peeling a
+ * single type off an `Option` union. TS 5.5+'s inferred type
+ * predicates fire here too, so a body like `typeof v === "string"`
+ * works without explicit `v is string` annotation.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const isEven = (n: number) => n % 2 === 0;
+ *
+ * expect(None().filter(isEven)).toEqual(None());
+ * expect(Some(3).filter(isEven)).toEqual(None());
+ * expect(Some(4).filter(isEven)).toEqual(Some(4));
+ *
+ * // Type narrowing via inferred predicate:
+ * const x: Option = Some("hi");
+ * const justStrings = x.filter(v => typeof v === "string");
+ * // justStrings: Option
+ * ```
+ *
+ * # Implementation note
+ *
+ * `filter` returns a fresh `Option` instead of narrowing `this` via a
+ * `this is X` predicate return. That's what lets TS 5.5+'s inferred type
+ * predicates fire on the implicit-lambda case — they don't fire on `this is
+ * X` returns. The trade is one extra `.isSome()` check downstream to access
+ * `.value`.
+ */
+ filter(this: Option, fn: (v: T) => v is U): Option;
+ /**
+ * Returns `None` if the option is `None`, otherwise calls `predicate` with
+ * the wrapped value and returns:
+ *
+ * - `Some(t)` if `predicate` returns `true` (where `t` is the wrapped value)
+ * - `None` if `predicate` returns `false`
+ *
+ * # Examples
+ *
+ * ```ts
+ * const isEven = (n: number) => n % 2 === 0;
+ *
+ * expect(None().filter(isEven)).toEqual(None());
+ * expect(Some(3).filter(isEven)).toEqual(None());
+ * expect(Some(4).filter(isEven)).toEqual(Some(4));
+ * ```
+ */
+ filter(this: Option, fn: (v: T) => unknown): Option;
+
+ /**
+ * Returns the option if it contains a value, otherwise returns `other`.
+ *
+ * Arguments passed to `or` are eagerly evaluated; if you are passing the
+ * result of a function call, it is recommended to use `orElse`, which is
+ * lazily evaluated.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const x: Option = Some(2);
+ * const y: Option = None();
+ * expect(x.or(y)).toEqual(Some(2));
+ *
+ * const a: Option = None();
+ * const b: Option = Some(100);
+ * expect(a.or(b)).toEqual(Some(100));
+ *
+ * const c: Option = Some(2);
+ * const d: Option = Some(100);
+ * expect(c.or(d)).toEqual(Some(2));
+ *
+ * const e: Option = None();
+ * const f: Option = None();
+ * expect(e.or(f)).toEqual(None());
+ * ```
+ */
+ or(this: Option, other: Option): Option;
+
+ /**
+ * Returns the option if it contains a value, otherwise calls `fallback`
+ * and returns the result.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const nobody = (): Option => None();
+ * const vikings = (): Option => Some("vikings");
+ *
+ * expect(Some("barbarians").orElse(vikings)).toEqual(Some("barbarians"));
+ * expect(None().orElse(vikings)).toEqual(Some("vikings"));
+ * expect(None().orElse(nobody)).toEqual(None());
+ * ```
+ */
+ orElse(this: Option, fallback: () => Option): Option;
+
+ /**
+ * Returns `Some` if exactly one of `this`, `other` is `Some`, otherwise
+ * returns `None`.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const x: Option = Some(2);
+ * const y: Option = None();
+ * expect(x.xor(y)).toEqual(Some(2));
+ *
+ * const a: Option = None();
+ * const b: Option = Some(2);
+ * expect(a.xor(b)).toEqual(Some(2));
+ *
+ * const c: Option = Some(2);
+ * const d: Option = Some(2);
+ * expect(c.xor(d)).toEqual(None());
+ *
+ * const e: Option = None();
+ * const f: Option = None();
+ * expect(e.xor(f)).toEqual(None());
+ * ```
+ */
+ xor(this: Option, other: Option): Option;
+
+ /**
+ * Converts from `Option>` to `Option`.
+ *
+ * Flattening only removes one level of nesting at a time.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const x: Option> = Some(Some(6));
+ * expect(x.flatten()).toEqual(Some(6));
+ *
+ * const y: Option > = Some(None());
+ * expect(y.flatten()).toEqual(None());
+ *
+ * const z: Option > = None();
+ * expect(z.flatten()).toEqual(None());
+ * ```
+ *
+ * Flattening only removes one level of nesting at a time:
+ *
+ * ```ts
+ * const x: Option >> = Some(Some(Some(6)));
+ * expect(x.flatten()).toEqual(Some(Some(6)));
+ * expect(x.flatten().flatten()).toEqual(Some(6));
+ * ```
+ */
+ flatten(this: Option>): Option;
+
+ /**
+ * Zips `this` with another `Option`.
+ *
+ * If `this` is `Some(s)` and `other` is `Some(o)`, this method returns
+ * `Some([s, o])`. Otherwise, `None` is returned.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const x = Some(1);
+ * const y = Some("hi");
+ * const z = None();
+ *
+ * expect(x.zip(y)).toEqual(Some([1, "hi"]));
+ * expect(x.zip(z)).toEqual(None());
+ * ```
+ */
+ zip(this: Option, other: Option): Option<[T, U]>;
+
+ /**
+ * Zips `this` and another `Option` with the provided function.
+ *
+ * If `this` is `Some(s)` and `other` is `Some(o)`, this method returns
+ * `Some(fn(s, o))`. Otherwise, `None` is returned.
+ *
+ * # Examples
+ *
+ * ```ts
+ * type Point = { x: number; y: number };
+ * const newPoint = (x: number, y: number): Point => ({ x, y });
+ *
+ * const xCoord = Some(17.5);
+ * const yCoord = Some(42.7);
+ *
+ * expect(xCoord.zipWith(yCoord, newPoint)).toEqual(Some({ x: 17.5, y: 42.7 }));
+ * expect(xCoord.zipWith(None(), newPoint)).toEqual(None());
+ * ```
+ */
+ zipWith(this: Option, other: Option, fn: (l: T, r: U) => R): Option;
+
+ /**
+ * Unzips an option containing a tuple of two options.
+ *
+ * If `this` is `Some([a, b])`, this method returns `[Some(a), Some(b)]`.
+ * Otherwise, `[None, None]` is returned.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const x = Some<[number, string]>([1, "hi"]);
+ * expect(x.unzip()).toEqual([Some(1), Some("hi")]);
+ *
+ * const y: Option<[number, string]> = None();
+ * expect(y.unzip()).toEqual([None(), None()]);
+ * ```
+ */
+ unzip(this: Option<[A, B]>): [Option , Option];
+
+ /**
+ * Returns an iterator over the contained value.
+ *
+ * If `this` is `Some(t)`, the iterator yields `t` exactly once. If `this`
+ * is `None`, the iterator yields nothing.
+ *
+ * Lets `Option` participate in JavaScript's native iterable protocols
+ * (`for...of`, spread, `Array.from`) as a zero-or-one-element sequence.
+ *
+ * # Examples
+ *
+ * ```ts
+ * const x = Some(4);
+ * for (const v of x) {
+ * expect(v).toEqual(4);
+ * }
+ *
+ * const y = None();
+ * for (const v of y) {
+ * throw new Error("None yields nothing — this line is unreachable");
+ * }
+ *
+ * expect([...Some(7)]).toEqual([7]);
+ * expect([...None()]).toEqual([]);
+ *
+ * expect(Array.from(Some("hi"))).toEqual(["hi"]);
+ * ```
+ */
+ [Symbol.iterator](this: Option): IterableIterator;
+};
+
+const mkOptionProto = (): OptionMethods =>
+ ({
+ isSome(this: Option): this is Some {
+ return this.__type === "Some";
+ },
+ isSomeAnd(this: Option, fn: (v: T) => boolean): boolean {
+ return this.isSome() && fn(this.value);
+ },
+ isNone(this: Option): this is None {
+ return this.__type === "None";
+ },
+ isNoneOr(this: Option, fn: (v: T) => boolean): boolean {
+ return !this.isSome() || fn(this.value);
+ },
+ expect(this: Option, message: string): T {
+ if (this.isSome()) {
+ return this.value;
+ }
+
+ throw new Error(message);
+ },
+ unwrap(this: Option): T {
+ return this.expect("should have been Some, found None");
+ },
+ unwrapOr(this: Option, fallback: T): T {
+ if (this.isSome()) {
+ return this.value;
+ }
+
+ return fallback;
+ },
+ unwrapOrElse(this: Option, fn: () => T): T {
+ if (this.isSome()) {
+ return this.value;
+ }
+
+ return fn();
+ },
+ map(this: Option, fn: (v: T) => U): Option {
+ if (this.isSome()) {
+ return Some(fn(this.value));
+ }
+
+ return None();
+ },
+ inspect(fn) {
+ if (this.isSome()) {
+ fn(this.value);
+ }
+
+ return this;
+ },
+ mapOr(fallback, fn) {
+ if (this.isSome()) {
+ return fn(this.value);
+ }
+
+ return fallback;
+ },
+ mapOrElse(fallback, fn) {
+ if (this.isSome()) {
+ return fn(this.value);
+ }
+
+ return fallback();
+ },
+ and(other) {
+ if (this.isSome()) {
+ return other;
+ }
+
+ return None();
+ },
+ andThen(fn) {
+ if (this.isSome()) {
+ return fn(this.value);
+ }
+
+ return None();
+ },
+ filter(this: Option, fn: (v: T) => boolean): Option {
+ if (this.isSome() && fn(this.value)) return this;
+ return None();
+ },
+ or(other) {
+ if (this.isSome()) {
+ return this;
+ }
+
+ return other;
+ },
+ orElse(fallback) {
+ if (this.isSome()) {
+ return this;
+ }
+
+ return fallback();
+ },
+ xor(other) {
+ // We can't call `other.isSome()` directly: TS sees `other`'s `Option`
+ // parameter as the expanded `Some | None`, which doesn't unify
+ // with the unsimplified distributive conditional that `isSome`'s
+ // `this` parameter expects for an unfixed T. Dropping to the `__type`
+ // discriminant sidesteps the issue — discriminant comparison narrows the
+ // union without going through a method call.
+ const otherIsSome = other.__type === "Some";
+ if (this.isSome() && !otherIsSome) return this;
+ if (!this.isSome() && otherIsSome) return other;
+ return None();
+ },
+ flatten(this: Option>): Option