Skip to content

Instantly share code, notes, and snippets.

@nberlette
Created March 14, 2025 09:10
Show Gist options
  • Save nberlette/c019b7b6c4767a517d00cdc08122cdba to your computer and use it in GitHub Desktop.
Save nberlette/c019b7b6c4767a517d00cdc08122cdba to your computer and use it in GitHub Desktop.
Option<T>
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