Created
March 14, 2025 09:10
-
-
Save nberlette/c019b7b6c4767a517d00cdc08122cdba to your computer and use it in GitHub Desktop.
Option<T>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const _tag: unique symbol = Symbol("Option.#tag"); | |
type _tag = typeof _tag; | |
export interface Some<T> extends Option<T> { | |
readonly [_tag]: "Some"; | |
readonly value: T; | |
} | |
export interface None extends Option<never> { | |
readonly [_tag]: "None"; | |
} | |
/** | |
* Represents an optional value: an instance can be "Some" with a value, or | |
* "None". | |
* | |
* This is the TypeScript analog of Rust's `Option<T>` type. Use | |
* `Option.some(...)` to create a "Some" variant, or `Option.none<T>()` to | |
* create a "None" variant. | |
* | |
* @example | |
* ```ts | |
* import { Option } from "./option.ts"; | |
* | |
* const a = Option.some(10); | |
* const b = Option.none<number>(); | |
* | |
* console.log(a.isSome()); // true | |
* console.log(b.isNone()); // true | |
* console.log(a.unwrap()); // 10 | |
* // console.log(b.unwrap()); // Throws an Error | |
* ``` | |
* @category Option | |
*/ | |
export class Option<T> { | |
/** | |
* Creates a new `Option` containing the provided non-null value. | |
* | |
* @example | |
* ```ts | |
* const x = Option.some("hello"); | |
* console.log(x.isSome()); // true | |
* ``` | |
* @category Constructors | |
*/ | |
static some<U>(value: U): Option<U> { | |
return new Option<U>(value, true); | |
} | |
/** | |
* Creates a new `Option` which contains no value. | |
* | |
* @example | |
* ```ts | |
* const x = Option.none<number>(); | |
* console.log(x.isNone()); // true | |
* ``` | |
* @category Constructors | |
*/ | |
static none(): None { | |
return new Option(null!, false); | |
} | |
#value: T | null = null; | |
#isSome = false; | |
/** | |
* Internal constructor. Use `Option.some(value)` or `Option.none()` instead. | |
*/ | |
private constructor( | |
...args: [value: T, some: true] | [value: never, some: false] | |
) { | |
[this.#value, this.#isSome = false] = args; | |
} | |
get [_tag](): [T] extends [never] ? "None" : "Some" { | |
return ( | |
this.#isSome ? "Some" : "None" | |
) as [T] extends [never] ? "None" : "Some"; | |
} | |
/** | |
* Returns `true` if this is a `Some` variant. | |
* | |
* @example | |
* ```ts | |
* const x = Option.some(123); | |
* console.log(x.isSome()); // true | |
* ``` | |
*/ | |
isSome(): this is Some<T> { | |
return this.#isSome; | |
} | |
/** | |
* Returns `true` if this is a `None` variant. | |
* | |
* @example | |
* ```ts | |
* const x = Option.none<string>(); | |
* console.log(x.isNone()); // true | |
* ``` | |
*/ | |
isNone(): this is None { | |
return !this.#isSome; | |
} | |
/** | |
* Returns `true` if this is `Some` and the wrapped value satisfies the | |
* given predicate. | |
* | |
* @example | |
* ```ts | |
* const x = Option.some(10); | |
* console.log(x.isSomeAnd((v) => v > 5)); // true | |
* ``` | |
*/ | |
isSomeAnd(predicate: (val: T) => boolean): boolean { | |
return this.#isSome && predicate(this.#value as T); | |
} | |
/** | |
* Returns `true` if this is `None` or the wrapped value satisfies the | |
* given predicate. In other words, returns false only if `Some(v)` and | |
* predicate fails. | |
* | |
* @example | |
* ```ts | |
* const x = Option.some(2); | |
* console.log(x.isNoneOr((v) => v > 1)); // true | |
* | |
* const y = Option.some(0); | |
* console.log(y.isNoneOr((v) => v > 1)); // false | |
* | |
* const z = Option.none<number>(); | |
* console.log(z.isNoneOr((v) => v > 1)); // true | |
* ``` | |
*/ | |
isNoneOr(predicate: (val: T) => boolean): boolean { | |
return this.isNone() || predicate(this.#value as T); | |
} | |
/** | |
* If this is `Some`, returns the contained value. Otherwise, throws an Error | |
* with the provided `msg`. | |
* | |
* @example | |
* ```ts | |
* const x = Option.some("hello"); | |
* console.log(x.expect("should have a string")); // "hello" | |
* | |
* const y = Option.none<string>(); | |
* // y.expect("where did my string go?"); // throws Error | |
* ``` | |
*/ | |
expect(msg: string): T { | |
if (this.#isSome) { | |
return this.#value as T; | |
} | |
throw new Error(msg); | |
} | |
/** | |
* If this is `Some`, returns the contained value; if this is `None`, throws an Error. | |
* | |
* @example | |
* ```ts | |
* const x = Option.some(42); | |
* console.log(x.unwrap()); // 42 | |
* | |
* const y = Option.none<number>(); | |
* // y.unwrap(); // throws Error | |
* ``` | |
*/ | |
unwrap(): T { | |
if (!this.#isSome) { | |
throw new Error("called `Option.unwrap()` on a `None` value"); | |
} | |
return this.#value as T; | |
} | |
/** | |
* Returns the contained value if `Some`, otherwise returns `defaultValue`. | |
* | |
* @example | |
* ```ts | |
* const x = Option.some(2); | |
* console.log(x.unwrapOr(10)); // 2 | |
* | |
* const y = Option.none<number>(); | |
* console.log(y.unwrapOr(10)); // 10 | |
* ``` | |
*/ | |
unwrapOr(defaultValue: T): T { | |
return this.#isSome ? (this.#value as T) : defaultValue; | |
} | |
/** | |
* Returns the contained value if `Some`, otherwise calls the provided | |
* function and returns that result. | |
* | |
* @example | |
* ```ts | |
* const fetchVal = () => 999; | |
* const x = Option.some(2); | |
* console.log(x.unwrapOrElse(fetchVal)); // 2 | |
* | |
* const y = Option.none<number>(); | |
* console.log(y.unwrapOrElse(fetchVal)); // 999 | |
* ``` | |
*/ | |
unwrapOrElse(fn: () => T): T { | |
return this.#isSome ? (this.#value as T) : fn(); | |
} | |
/** | |
* Maps an `Option<T>` to `Option<U>` by applying the function to the | |
* contained value if `Some`, or returns `None` otherwise. | |
* | |
* @example | |
* ```ts | |
* const x = Option.some(10); | |
* console.log(x.map((v) => v.toString())); // Some("10") | |
* | |
* const y = Option.none<number>(); | |
* console.log(y.map((v) => v.toString())); // None | |
* ``` | |
*/ | |
map<U>(fn: (val: T) => U): Option<U> { | |
if (!this.#isSome) return Option.none(); | |
return Option.some(fn(this.#value as T)); | |
} | |
/** | |
* Returns `defaultVal` if `None`, or applies `fn` to the contained value | |
* if `Some`. | |
* | |
* @example | |
* ```ts | |
* const x = Option.some("hello"); | |
* console.log(x.mapOr(0, (v) => v.length)); // 5 | |
* | |
* const y = Option.none<string>(); | |
* console.log(y.mapOr(0, (v) => v.length)); // 0 | |
* ``` | |
*/ | |
mapOr<U>(defaultVal: U, fn: (val: T) => U): U { | |
return this.#isSome ? fn(this.#value as T) : defaultVal; | |
} | |
/** | |
* Returns the result of `defaultFn()` if `None`, or applies `fn` to the | |
* contained value if `Some`. | |
* | |
* @example | |
* ```ts | |
* const x = Option.some("abc"); | |
* console.log(x.mapOrElse(() => 0, (v) => v.length)); // 3 | |
* | |
* const y = Option.none<string>(); | |
* console.log(y.mapOrElse(() => 0, (v) => v.length)); // 0 | |
* ``` | |
*/ | |
mapOrElse<U>(defaultFn: () => U, fn: (val: T) => U): U { | |
return this.#isSome ? fn(this.#value as T) : defaultFn(); | |
} | |
/** | |
* Calls `fn` with a reference to the contained value (if `Some`), returning | |
* the original `Option`. | |
* | |
* Useful for side effects like debugging/logging. | |
* | |
* @example | |
* ```ts | |
* Option.some(5) | |
* .inspect((x) => console.log("value is", x)) | |
* .map((x) => x * 2); | |
* ``` | |
*/ | |
inspect(fn: (val: T) => void): this { | |
if (this.#isSome) { | |
fn(this.#value as T); | |
} | |
return this; | |
} | |
/** | |
* Returns `None` if this is `None`; otherwise calls `fn` on the wrapped value | |
* and returns the result. | |
* | |
* Also known as "flatMap". | |
* | |
* @example | |
* ```ts | |
* const sqThenString = (x: number) => { | |
* const squared = x * x; | |
* return Option.some(squared.toString()); | |
* }; | |
* | |
* const a = Option.some(2).andThen(sqThenString); | |
* console.log(a); // Some("4") | |
* | |
* const b = Option.none<number>().andThen(sqThenString); | |
* console.log(b); // None | |
* ``` | |
*/ | |
andThen<U>(fn: (val: T) => Option<U>): Option<U> { | |
if (!this.#isSome) return Option.none<U>(); | |
return fn(this.#value as T); | |
} | |
/** | |
* Returns `None` if this is `None`; otherwise returns `optb`. | |
* | |
* @example | |
* ```ts | |
* const x = Option.some(1); | |
* const y = Option.some("hello"); | |
* console.log(x.and(y)); // Some("hello") | |
* | |
* const z = Option.none<number>(); | |
* console.log(x.and(z)); // None | |
* ``` | |
*/ | |
and<U>(optb: Option<U>): Option<U> { | |
if (!this.#isSome) return Option.none<U>(); | |
return optb; | |
} | |
/** | |
* Returns the original `Option` if it's `Some`, otherwise returns `optb`. | |
* | |
* @example | |
* ```ts | |
* const x = Option.some(1); | |
* const y = Option.some(2); | |
* console.log(x.or(y)); // Some(1) | |
* | |
* const z = Option.none<number>(); | |
* console.log(z.or(y)); // Some(2) | |
* ``` | |
*/ | |
or(optb: Option<T>): Option<T> { | |
return this.#isSome ? this : optb; | |
} | |
/** | |
* Returns the original `Option` if it's `Some`, otherwise calls `fn` | |
* and returns that `Option`. | |
* | |
* @example | |
* ```ts | |
* const fallback = () => Option.some(99); | |
* | |
* const x = Option.some(10); | |
* console.log(x.orElse(fallback)); // Some(10) | |
* | |
* const y = Option.none<number>(); | |
* console.log(y.orElse(fallback)); // Some(99) | |
* ``` | |
*/ | |
orElse(fn: () => Option<T>): Option<T> { | |
return this.#isSome ? this : fn(); | |
} | |
/** | |
* Returns `Some` if exactly one of `this`, `optb` is `Some`, | |
* otherwise returns `None`. | |
* | |
* @example | |
* ```ts | |
* const a = Option.some(1); | |
* const b = Option.none<number>(); | |
* console.log(a.xor(b)); // Some(1) | |
* | |
* const c = Option.some(2); | |
* const d = Option.some(3); | |
* console.log(c.xor(d)); // None | |
* ``` | |
*/ | |
xor(optb: Option<T>): Option<T> { | |
if (this.#isSome && !optb.#isSome) { | |
return this; | |
} else if (!this.#isSome && optb.#isSome) { | |
return optb; | |
} | |
return Option.none<T>(); | |
} | |
/** | |
* Returns `None` if this is `None` or if the predicate returns `false` | |
* for the wrapped value. Otherwise returns `this`. | |
* | |
* @example | |
* ```ts | |
* const x = Option.some(10); | |
* console.log(x.filter((v) => v > 5)); // Some(10) | |
* console.log(x.filter((v) => v < 5)); // None | |
* ``` | |
*/ | |
filter(predicate: (val: T) => boolean): Option<T> { | |
if (!this.#isSome) return this; | |
return predicate(this.#value as T) ? this : Option.none<T>(); | |
} | |
/** | |
* Takes the value out if `Some`, leaving `None` in its place, | |
* and returns the taken value as a new `Option`. | |
* | |
* @example | |
* ```ts | |
* const x = Option.some(5); | |
* const y = x.take(); | |
* console.log(x.isNone()); // true | |
* console.log(y.isSome()); // true, with value 5 | |
* ``` | |
*/ | |
take(): Option<T> { | |
if (!this.#isSome) { | |
return Option.none<T>(); | |
} | |
const val = this.#value as T; | |
this.#value = null; | |
this.#isSome = false; | |
return Option.some(val); | |
} | |
/** | |
* If the predicate returns `true`, takes the value out, leaving `None`. | |
* Otherwise returns `None` without taking anything. Returns the taken | |
* `Option` if actually taken, or `None` otherwise. | |
* | |
* @example | |
* ```ts | |
* const x = Option.some(42); | |
* const taken = x.takeIf((val) => val === 42); | |
* console.log(x.isNone()); // true | |
* console.log(taken.unwrap()); // 42 | |
* ``` | |
*/ | |
takeIf(predicate: (val: T) => boolean): Option<T> { | |
if (!this.#isSome) { | |
return Option.none<T>(); | |
} | |
if (predicate(this.#value as T)) { | |
return this.take(); | |
} | |
return Option.none<T>(); | |
} | |
/** | |
* Replaces the wrapped value with the provided one, returning an `Option` | |
* of the old value. | |
* | |
* @example | |
* ```ts | |
* const x = Option.some(1); | |
* const old = x.replace(99); | |
* console.log(x.unwrap()); // 99 | |
* console.log(old.unwrap()); // 1 | |
* ``` | |
*/ | |
replace(newValue: T): Option<T> { | |
const old = this.#isSome ? Option.some(this.#value as T) : Option.none<T>(); | |
this.#value = newValue; | |
this.#isSome = true; | |
return old; | |
} | |
/** | |
* Converts an `Option<Option<U>>` to `Option<U>` by removing one level | |
* of nesting. If this is `Some(None)`, the result becomes `None`. | |
* | |
* @example | |
* ```ts | |
* const nested = Option.some(Option.some(42)); | |
* console.log(nested.flatten()); // Some(42) | |
* | |
* const noneNested = Option.some(Option.none<number>()); | |
* console.log(noneNested.flatten()); // None | |
* ``` | |
*/ | |
flatten<U>(this: Option<Option<U>>): Option<U> { | |
if (!this.#isSome) return Option.none<U>(); | |
return (this.#value as Option<U>); | |
} | |
/** | |
* If both this and `optb` are `Some`, returns `Some<[T, U]>` (a pair), | |
* otherwise returns `None`. | |
* | |
* @example | |
* ```ts | |
* const a = Option.some(1); | |
* const b = Option.some("hello"); | |
* console.log(a.zip(b)); // Some([1, "hello"]) | |
* | |
* const c = Option.none<number>(); | |
* console.log(a.zip(c)); // None | |
* ``` | |
*/ | |
zip<U>(optb: Option<U>): Option<[T, U]> { | |
if (this.#isSome && optb.#isSome) { | |
return Option.some([this.#value as T, optb.#value as U]); | |
} | |
return Option.none<[T, U]>(); | |
} | |
/** | |
* If both this and `optb` are `Some`, calls `fn` with both values and | |
* returns `Some(fn(a, b))`. Otherwise returns `None`. | |
* | |
* @example | |
* ```ts | |
* const a = Option.some(17.5); | |
* const b = Option.some(42.7); | |
* const point = a.zipWith(b, (x, y) => ({ x, y })); | |
* // Some({ x: 17.5, y: 42.7 }) | |
* | |
* const c = Option.none<number>(); | |
* console.log(a.zipWith(c, (x, y) => x + y)); // None | |
* ``` | |
*/ | |
zipWith<U, R>( | |
optb: Option<U>, | |
fn: (a: T, b: U) => R, | |
): Option<R> { | |
if (this.#isSome && optb.#isSome) { | |
return Option.some(fn(this.#value as T, optb.#value as U)); | |
} | |
return Option.none<R>(); | |
} | |
/** | |
* Returns a string representation for debugging or logging. | |
* | |
* @example | |
* ```ts | |
* console.log(Option.none<number>().toString()); // "None" | |
* console.log(Option.some(123).toString()); // "Some(123)" | |
* ``` | |
*/ | |
toString(): string { | |
if (this.#isSome) { | |
return `Some(${String(this.#value)})`; | |
} | |
return "None"; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment