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 { + if (this.isSome()) { + return this.value; + } + return None(); + }, + zip(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. + if (this.isSome() && other.__type === "Some") { + return Some([this.value, other.value]); + } + return None(); + }, + zipWith(other, fn) { + // 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. + if (this.isSome() && other.__type === "Some") { + return Some(fn(this.value, other.value)); + } + return None(); + }, + unzip(this: Option<[A, B]>): [Option, Option] { + if (this.isSome()) { + const [a, b] = this.value; + return [Some(a), Some(b)]; + } + return [None(), None()]; + }, + [Symbol.iterator](this: Option): IterableIterator { + // Defer to a native array iterator over either `[value]` (Some) or + // `[]` (None). Sidesteps two issues at once: a generator method + // would inferred-return `Generator` and trip the parent cast on + // the literal, and aliasing `this` to a local would trigger + // `no-this-alias` from oxlint. The tiny per-call allocation is + // worth the simplicity. + // + // We branch on `.isSome()` rather than `.__type === "Some"` here + // because TS's narrowing through a discriminant check on a + // distributive `Option` with unfixed T doesn't reliably surface + // `.value` on the Some branch; the method-based type guard does. + if (this.isSome()) { + return [this.value][Symbol.iterator](); + } + return [][Symbol.iterator]() as IterableIterator; + }, + }) as OptionMethods; + +const OptionProto = mkOptionProto(); + +/** + * The `Option` type. Represents an optional value: every `Option` is + * either `Some` and contains a value of type `T`, or `None`, and does not. + * + * `Option`s are commonly paired with narrowing via `isSome()` / `isNone()` + * to query the presence of a value and take action, always accounting for + * the `None` case. See the module-level documentation for more. + * + * `Option` is **distributive over `T`**: `Option` resolves to + * `Option | Option`. This lets type-guard predicates passed to + * `filter` subtract a single variant out of a union (e.g. peeling + * `string` off an `Option` to get `Option`). + * + * @template T The type of the contained value when the option is `Some`. + */ +export type Option = T extends unknown ? Some | None : never; + +/** + * Constructs a `Some` variant containing the given value. + * + * The return is typed as `Option` (rather than the more-specific + * `Some`) so the result can be assigned to either variant of a wider + * union without further annotation. + * + * # Examples + * + * ```ts + * const x = Some(42); + * expect(x.isSome()).toBe(true); + * + * // Use an explicit type argument to widen T when the literal value + * // would otherwise narrow it too far: + * let y: Option = Some(0); + * ``` + * + * @template T The type of the contained value. + */ +export const Some = (value: T): Option => + Object.setPrototypeOf({ __type: "Some", value }, OptionProto) as Option; + +// None is internally a singleton — there's no T-bearing state at +// runtime, so we allocate the object once at module load and hand back +// the same instance for every `None()` call. The function wrapper +// preserves the symmetry with `Some(value)`: both look like +// constructors at the call site and both return `Option`. +const NONE_INSTANCE = Object.setPrototypeOf({ __type: "None" }, OptionProto) as None; + +/** + * Constructs a `None` variant. + * + * All `None()` calls return the same internal singleton — there is no + * T-bearing state at runtime, so `None() === None()` is + * `true`. The `` type parameter exists so the return can be + * assigned to any `Option` without an explicit annotation. + * + * # Examples + * + * ```ts + * const x: Option = None(); + * expect(x.isNone()).toBe(true); + * + * // Identity is preserved across calls: + * expect(None()).toBe(None()); + * ``` + * + * @template T The "phantom" element type. Defaults to `never` because the + * `None` variant doesn't carry a value. + */ +export const None = (): Option => NONE_INSTANCE as Option; + +/** + * Converts a nullable value into an `Option`. + * + * Treats both `null` and `undefined` as `None`; any other value wraps as + * `Some(value)`. This is the workhorse for wrapping browser-spec APIs that + * return `T | null` or `T | undefined`. + * + * # Examples + * + * ```ts + * expect(Option.from(42)).toEqual(Some(42)); + * expect(Option.from(null)).toEqual(None()); + * expect(Option.from(undefined)).toEqual(None()); + * + * // Wrapping a DOM call: + * const root = Option.from(document.querySelector("#root")); + * + * // Shorthand call form — same behavior: + * const stored = Option(localStorage.getItem("theme")); + * ``` + */ +const optionFromNullable = (value: T | null | undefined): Option => + value === null || value === undefined ? None() : Some(value); + +/** + * The `Option` namespace value. Provides three forms of access to the + * type's constructors and conversions: + * + * - **Callable shorthand**: `Option(x)` wraps a `T | null | undefined` as + * `Option`. Equivalent to `Option.from(x)`. The high-leverage form + * for wrapping browser-spec APIs that return nullable values. + * - **Namespace access**: `Option.Some(value)` and `Option.None()` are + * the variant constructors. Useful when importing a single `Option` + * binding and reaching for everything through it. + * - **Explicit conversion**: `Option.from(x)` is the named form of the + * callable shorthand. More discoverable in IDE autocomplete. + * + * The object is `Object.freeze`d, so the members can't be reassigned. + * + * # Examples + * + * ```ts + * // All four forms produce equivalent values: + * Option(42); // Some(42) + * Option.from(42); // Some(42) + * Option.Some(42); // Some(42) + * Some(42); // Some(42), if you've imported `Some` directly + * + * // null / undefined map to None: + * Option(null); // None + * Option(undefined); // None + * Option.None(); // None + * ``` + */ +export const Option = Object.freeze( + Object.assign(optionFromNullable, { + Some, + None, + from: optionFromNullable, + }), +); diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 46235b3..3f538a8 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -5,13 +5,6 @@ "rootDir": "src", "outDir": "dist" }, - "include": [ - "src" - ], - "exclude": [ - "node_modules", - "dist", - "src/**/*.test.ts", - "src/**/*.test.tsx" - ] -} \ No newline at end of file + "include": ["src"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/**/*.test.tsx"] +} diff --git a/packages/tsconfig/base.json b/packages/tsconfig/base.json index cc02c6d..9498c43 100644 --- a/packages/tsconfig/base.json +++ b/packages/tsconfig/base.json @@ -4,7 +4,7 @@ "noUncheckedSideEffectImports": true, "exactOptionalPropertyTypes": true, "noUncheckedIndexedAccess": true, - "isolatedDeclarations": true, + "isolatedDeclarations": false, "declarationMap": true, "skipLibCheck": true, "declaration": true, @@ -12,6 +12,6 @@ "strict": true, "moduleResolution": "bundler", "module": "preserve", - "target": "es2025", + "target": "es2025" } -} \ No newline at end of file +} diff --git a/packages/tsconfig/library.json b/packages/tsconfig/library.json index 8ba8a4e..75c0167 100644 --- a/packages/tsconfig/library.json +++ b/packages/tsconfig/library.json @@ -2,6 +2,6 @@ "$schema": "http://json.schemastore.org/tsconfig", "extends": "./base.json", "compilerOptions": { - "noEmit": false, + "noEmit": false } -} \ No newline at end of file +} diff --git a/packages/tsconfig/package.json b/packages/tsconfig/package.json index 06de008..ccaf0ff 100644 --- a/packages/tsconfig/package.json +++ b/packages/tsconfig/package.json @@ -3,8 +3,8 @@ "name": "@phaseshift/tsconfig", "version": "0.0.0", "private": true, - "packageManager": "pnpm@10.29.2", "files": [ "*.json" - ] -} \ No newline at end of file + ], + "packageManager": "pnpm@10.29.2" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b581098..8be61dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,15 +17,15 @@ importers: turbo: specifier: latest version: 2.9.18 + typescript: + specifier: ^6.0.3 + version: 6.0.3 packages/core: devDependencies: '@phaseshift/tsconfig': specifier: workspace:* version: link:../tsconfig - typescript: - specifier: ^6.0.3 - version: 6.0.3 vitest: specifier: ^4.1.9 version: 4.1.9(vite@8.0.16) diff --git a/turbo.json b/turbo.json index 7d88903..576bae1 100644 --- a/turbo.json +++ b/turbo.json @@ -2,17 +2,11 @@ "$schema": "https://turborepo.dev/schema.json", "tasks": { "build": { - "dependsOn": [ - "^build" - ], - "outputs": [ - "dist/**" - ] + "dependsOn": ["^build"], + "outputs": ["dist/**"] }, "test": { - "dependsOn": [ - "build" - ] + "dependsOn": ["build"] }, "lint": { "cache": false @@ -23,4 +17,4 @@ "cache": false } } -} \ No newline at end of file +}