diff --git a/.idea/deno.xml b/.idea/deno.xml
index 42c1442..8df8236 100644
--- a/.idea/deno.xml
+++ b/.idea/deno.xml
@@ -1,7 +1,7 @@
-
+
\ No newline at end of file
diff --git a/deno.jsonc b/deno.jsonc
index 49115ad..f42153f 100644
--- a/deno.jsonc
+++ b/deno.jsonc
@@ -1,6 +1,7 @@
{
"name": "@suddenly-giovanni/std",
"version": "0.0.4",
+ "lock": true,
"exports": {
"./predicate": "./src/predicate/mod.ts",
"./option": "./src/option/mod.ts"
diff --git a/deno.lock b/deno.lock
index cc43d20..72ef3ec 100644
--- a/deno.lock
+++ b/deno.lock
@@ -3,6 +3,8 @@
"packages": {
"specifiers": {
"jsr:@std/assert": "jsr:@std/assert@0.225.3",
+ "jsr:@std/assert@^0.225.3": "jsr:@std/assert@0.225.3",
+ "jsr:@std/expect": "jsr:@std/expect@0.224.2",
"jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.0",
"jsr:@std/testing": "jsr:@std/testing@0.224.0",
"npm:@biomejs/biome": "npm:@biomejs/biome@1.7.3",
@@ -15,6 +17,13 @@
"jsr:@std/internal@^1.0.0"
]
},
+ "@std/expect@0.224.2": {
+ "integrity": "789ba764319503b1669b54fad2d279f35fe708247548878c5fb44e0cacc40a8e",
+ "dependencies": [
+ "jsr:@std/assert@^0.225.3",
+ "jsr:@std/internal@^1.0.0"
+ ]
+ },
"@std/internal@1.0.0": {
"integrity": "ac6a6dfebf838582c4b4f61a6907374e27e05bedb6ce276e0f1608fe84e7cd9a"
},
diff --git a/src/internal/function.ts b/src/internal/function.ts
index 27d3a64..3462759 100644
--- a/src/internal/function.ts
+++ b/src/internal/function.ts
@@ -1,5 +1,32 @@
// deno-lint-ignore-file ban-types
+/**
+ * A lazy computation that can be used to defer the evaluation of an expression.
+ *
+ * @example
+ * To defer the evaluation of an expression:
+ * ```ts
+ * import { type Lazy } from './function.ts'
+ * declare function fib(n: number): number
+ *
+ * const lazy: Lazy = () => fib(100) // fib is not called yet, hence the computation is deferred
+ * ```
+ *
+ * @example
+ * As a parameter type in a function:
+ * ```ts
+ * import { type Lazy } from './function.ts'
+ * function defer(f: Lazy): A {
+ * return f()
+ * }
+ * const value = defer(() => 1) // 1
+ * ```
+ */
+export interface Lazy {
+ // biome-ignore lint/style/useShorthandFunctionType: we want the opaque type here
+ (): A
+}
+
/**
* Pipes the value of an expression into a pipeline of functions.
*
diff --git a/src/internal/types.test.ts b/src/internal/types.test.ts
new file mode 100644
index 0000000..67056ec
--- /dev/null
+++ b/src/internal/types.test.ts
@@ -0,0 +1,23 @@
+import { expectTypeOf } from 'npm:expect-type@0.19.0'
+import { describe, test } from 'jsr:@std/testing/bdd'
+import type { Types } from './types.ts'
+
+describe('Types', () => {
+ test('Tag', () => {
+ expectTypeOf & unknown>().toEqualTypeOf<
+ 'a' | 'b'
+ >()
+ })
+
+ test('Equal', () => {
+ expectTypeOf>().toEqualTypeOf()
+ expectTypeOf>().toEqualTypeOf()
+ })
+
+ test('Simplify', () => {
+ expectTypeOf>().toEqualTypeOf<{
+ a: number
+ b: number
+ }>()
+ })
+})
diff --git a/src/internal/types.ts b/src/internal/types.ts
new file mode 100644
index 0000000..7abaea4
--- /dev/null
+++ b/src/internal/types.ts
@@ -0,0 +1,66 @@
+/**
+ * This namespace provides utility types for type manipulation.
+ *
+ * @example
+ * ```ts
+ * import type { Types } from './types.ts'
+ *
+ * type Test1 = Types.Tags
+ * // ^? "a" | "b"
+ *
+ * type Test2 = Types.Equals<{ a: number }, { a: number }>
+ * // ^? true
+ *
+ * type Test3 = Types.Simplify<{ a: number } & { b: number }>
+ * // ^? { a: number; b: number; }
+ *
+ * ```
+ */
+export declare namespace Types {
+ /**
+ * Returns the tags in a type.
+ *
+ * @example
+ * ```ts
+ * import type { Types } from './types.ts'
+ *
+ * type Res = Types.Tags // "a" | "b"
+ * ```
+ *
+ * @category types
+ */
+ export type Tags = E extends { _tag: string } ? E['_tag'] : never
+
+ /**
+ * Determines if two types are equal.
+ *
+ * @example
+ * ```ts
+ * import type { Types } from './types.ts'
+ *
+ * type Res1 = Types.Equals<{ a: number }, { a: number }> // true
+ * type Res2 = Types.Equals<{ a: number }, { b: number }> // false
+ * ```
+ *
+ * @category models
+ */
+ export type Equals = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2
+ ? true
+ : false
+
+ /**
+ * Simplifies the type signature of a type.
+ *
+ * @example
+ * ```ts
+ * import type { Types } from './types.ts'
+ *
+ * type Res = Types.Simplify<{ a: number } & { b: number }> // { a: number; b: number; }
+ * ```
+ * @category types
+ */
+ export type Simplify = {
+ [K in keyof A]: A[K]
+ } extends infer B ? B
+ : never
+}
diff --git a/src/option/mod.ts b/src/option/mod.ts
index eb94524..f3f19cc 100644
--- a/src/option/mod.ts
+++ b/src/option/mod.ts
@@ -1 +1,44 @@
+/**
+ * # Option Module
+ * The Option Module is inspired by the Scala 3 Option type. This module primarily solves one of the common problems in programming - avoiding null and undefined values.
+ *
+ * The Option type is a container that encapsulates an optional value, i.e., it stores some value or none. It's a safer alternative to using null or undefined. By using Option, you can avoid null reference errors. The main advantage of using an `Option` type is that you're explicitly dealing with something that may or may not be there.
+ *
+ * ## How it works
+ * This module exports a base abstract class `Option` and two concrete subclasses `Some` and `None`. An `Option` object encapsulates a value that may or may not be present. A `Some` object represents an `Option` that contains a value, and `None` represents an `Option` that has no value.
+ *
+ * ## How to use it
+ * When you have a value that you want to lift into a boxed `Option`, you create an object of type `Some`, as follows:
+ *
+ * ```ts
+ * import { Option } from './option.ts'
+ *
+ * const value: Option.Type = Option.Some("Hello, world!");
+ * ```
+ *
+ * If there isn't a value to lift, you create a `None` object, as follows:
+ *
+ * ```ts
+ * import { Option } from './option.ts'
+ *
+ * const none: Option.Type = Option.None();
+ * ```
+ *
+ * To check the contents of an `Option`, use the `isSome` and `isNone` methods and extract the value when it is `Some`:
+ *
+ * ```ts
+ * import { Option } from './option.ts'
+ *
+ * const none: Option.Type = Option.None();
+ *
+ * if (none.isSome()) {
+ * console.log(none.get()); // this will throw an error !!!
+ * } else {
+ * console.log("Value is None");
+ * }
+ * ```
+ *
+ * @module
+ */
+
export { Option } from './option.ts'
diff --git a/src/option/option.test.ts b/src/option/option.test.ts
index 6ba7be3..c688e48 100644
--- a/src/option/option.test.ts
+++ b/src/option/option.test.ts
@@ -1,82 +1,275 @@
-import { assert, assertEquals, assertStrictEquals, assertThrows, equal } from 'jsr:@std/assert'
+import { expectTypeOf } from 'npm:expect-type@0.19.0'
+import { assertEquals, equal } from 'jsr:@std/assert'
+import { expect } from 'jsr:@std/expect'
import { describe, it, test } from 'jsr:@std/testing/bdd'
+
+import { pipe } from '../internal/function.ts'
+import type * as F from '../internal/function.ts'
import { Option } from './option.ts'
+// deno-lint-ignore no-namespace
+namespace Util {
+ // biome-ignore lint/suspicious/noExportsInTest:
+ export const deepStrictEqual = (actual: A, expected: A) => {
+ assertEquals(actual, expected)
+ }
+}
+
describe('Option', () => {
describe('constructors', () => {
- it('should throw an error when trying to construct Option directly ', () => {
- assertThrows(() => {
+ test('should throw an error when trying to construct Option directly ', () => {
+ expect(() => {
// @ts-expect-error - TSC does not allow instantiation of abstract classes, but what about runtime?
new Option()
- })
+ }).toThrow('Option is not meant to be instantiated directly')
})
- it('Some', () => {
+ test('Some', () => {
const some = Option.Some(1)
- assertStrictEquals(some._tag, 'Some')
- assertStrictEquals(some.value, 1)
+ expect(some._tag).toBe('Some')
+ expect(some.get()).toBe(1)
})
describe('None', () => {
- it('constructor', () => {
+ test('constructor', () => {
const none = Option.None()
- assertStrictEquals(none._tag, 'None')
+ expect(none._tag).toBe('None')
})
- it('None is a singleton ', () => {
+ test('None is a singleton ', () => {
const none = Option.None()
- assert(Option.None() === none)
- assert(Object.is(Option.None(), Option.None()))
+ expect(Option.None()).toStrictEqual(none)
+ expect(Object.is(Option.None(), Option.None())).toBe(true)
+ })
+ })
+
+ test('fromNullable', () => {
+ expect(Option.fromNullable(2).equals(Option.Some(2))).toBe(true)
+ expectTypeOf(Option.fromNullable(2)).toEqualTypeOf>()
+
+ expect(Option.fromNullable(0).equals(Option.Some(0))).toBe(true)
+ expectTypeOf(Option.fromNullable(0)).toEqualTypeOf>()
+
+ expect(Option.fromNullable('').equals(Option.Some(''))).toBe(true)
+ expectTypeOf(Option.fromNullable('')).toEqualTypeOf>()
+
+ expect(Option.fromNullable([]).equals(Option.Some([]), equal)).toBe(true)
+ expectTypeOf(Option.fromNullable([])).toEqualTypeOf>()
+ expectTypeOf(Option.fromNullable(['foo'])).toEqualTypeOf>()
+
+ const nullOption = Option.fromNullable(null)
+ expect(nullOption.isNone()).toBe(true)
+ expectTypeOf(nullOption).toEqualTypeOf>()
+
+ const undefinedOption = Option.fromNullable(undefined)
+ expect(undefinedOption.isNone()).toBe(true)
+ expectTypeOf(undefinedOption).toEqualTypeOf>()
+
+ interface Bar {
+ readonly baz?: null | number
+ }
+
+ expectTypeOf(Option.fromNullable({} as undefined | Bar)).toEqualTypeOf<
+ Option.Type<{
+ readonly baz?: null | number
+ }>
+ >()
+ expectTypeOf(Option.fromNullable({} as Bar)).toEqualTypeOf<
+ Option.Type<{
+ readonly baz?: null | number
+ }>
+ >()
+ expectTypeOf(Option.fromNullable(undefined as undefined | string)).toEqualTypeOf<
+ Option.Type
+ >()
+ expectTypeOf(Option.fromNullable(null as null | number)).toEqualTypeOf<
+ Option.Type
+ >()
+ })
+ })
+
+ describe('Option type namespace', () => {
+ test('Option.Value', () => {
+ const someOfNumber = Option.Some(1)
+ expectTypeOf>().toEqualTypeOf()
+
+ const noneOfUnknown = Option.None()
+ const noneOfNumber = Option.None()
+ expectTypeOf>().toEqualTypeOf()
+ expectTypeOf>().toEqualTypeOf()
+
+ type Foo = { foo: string }
+ const someOfRecord = Option.Some({ foo: 'bar' } satisfies Foo)
+ expectTypeOf>().toEqualTypeOf()
+ })
+ })
+
+ describe('get', () => {
+ test('Some', () => {
+ expect(Option.Some(1).get()).toBe(1)
+ expect(Option.Some({ foo: 'bar' }).get()).toEqual({ foo: 'bar' })
+ })
+
+ test('None', () => {
+ expect(() => Option.None().get()).toThrow('None.get')
+ })
+ })
+
+ describe('getOrElse', () => {
+ test('Some', () => {
+ expect(Option.Some(1).getOrElse(() => 2)).toBe(1)
+ expect(Option.Some({ foo: 'bar' }).getOrElse(() => ({ foo: 'baz' }))).toEqual({
+ foo: 'bar',
+ })
+
+ expectTypeOf(Option.Some({ foo: 'bar' }).getOrElse(() => ({ foo: 'baz' })))
+ .toEqualTypeOf<{
+ foo: string
+ }>()
+
+ expectTypeOf(
+ Option.Some({ foo: 'bar' }).getOrElse(() => ({ foo: 'baz' })),
+ ).not.toEqualTypeOf<{ foo: number }>() // it should extend the type Option instance type
+ })
+
+ test('None', () => {
+ expect(Option.None().getOrElse(() => 2)).toBe(2)
+ expect(
+ Option.fromNullable(undefined).getOrElse(() => ({
+ foo: 'baz',
+ })),
+ ).toEqual({
+ foo: 'baz',
})
+ expect(Option.None().getOrElse(() => null)).toEqual(null)
+ })
+ })
+
+ describe('fold', () => {
+ const fa = (s: string): { length: number } => ({ length: s.length })
+ const ifEmpty: F.Lazy = () => 42
+
+ it('returns call the ifEmpty for None cases ', () => {
+ const stringOption = Option.fromNullable(null)
+
+ // test the instance method
+ Util.deepStrictEqual(
+ stringOption.fold(ifEmpty, (_) => {
+ throw new Error('Called `absurd` function which should be un-callable')
+ }),
+ 42,
+ )
+
+ expectTypeOf(
+ stringOption.fold(ifEmpty, (_): boolean => {
+ throw new Error('Called `absurd` function which should be un-callable')
+ }),
+ ).toEqualTypeOf()
+
+ // test the static method
+ Util.deepStrictEqual(
+ pipe(
+ null as null | string,
+ Option.fromNullable,
+ Option.fold(ifEmpty, (_) => {
+ throw new Error('Called `absurd` function which should be un-callable')
+ }),
+ ),
+ 42,
+ )
+
+ expectTypeOf(
+ pipe(
+ stringOption,
+ Option.fold(ifEmpty, (_) => {
+ throw new Error('Called `absurd` function which should be un-callable')
+ }),
+ ),
+ ).toEqualTypeOf(42)
+ })
+
+ it('should call `f` for the `Some` case', () => {
+ const stringOption = Option.fromNullable('abc')
+ // test the instance method
+ Util.deepStrictEqual(stringOption.fold(ifEmpty, fa), { length: 3 })
+ expectTypeOf(stringOption.fold(ifEmpty, fa)).toEqualTypeOf<
+ number | { length: number }
+ >()
+
+ // test the static method
+ Util.deepStrictEqual(
+ pipe('abc' as null | string, Option.fromNullable, Option.fold(ifEmpty, fa)),
+ { length: 3 },
+ )
+ expectTypeOf(Option.fold(ifEmpty, fa)(stringOption)).toEqualTypeOf<
+ number | { length: number }
+ >()
})
+ })
- it('fromNullable', () => {
- assertStrictEquals(Option.fromNullable(2).equals(Option.Some(2)), true)
- assertStrictEquals(Option.fromNullable(0).equals(Option.Some(0)), true)
- assertStrictEquals(Option.fromNullable('').equals(Option.Some('')), true)
- assertStrictEquals(Option.fromNullable([]).equals(Option.Some([]), equal), true)
+ describe('match', () => {
+ const onNone = () => 'none' as const
+ const onSome = (s: string): number => s.length
+ test('static', () => {
+ Util.deepStrictEqual(
+ pipe(null as null | string, Option.fromNullable, Option.match({ onNone, onSome })),
+ 'none',
+ )
+ Util.deepStrictEqual(pipe('abc', Option.Some, Option.match({ onNone, onSome })), 3)
- assertStrictEquals(Option.isNone(Option.fromNullable(null)), true)
- assertStrictEquals(Option.fromNullable(undefined).isNone(), true)
+ expectTypeOf(Option.match({ onNone, onSome })(Option.fromNullable(null))).toEqualTypeOf<
+ number | 'none'
+ >()
+ })
+ test('instance', () => {
+ Util.deepStrictEqual(Option.None().match({ onNone, onSome }), 'none')
+ Util.deepStrictEqual(Option.Some('abc').match({ onNone, onSome }), 3)
+ expectTypeOf(Option.fromNullable(null).match({ onNone, onSome })).toEqualTypeOf<
+ number | 'none'
+ >()
})
})
describe('guards', () => {
- it('isOption', () => {
- assertStrictEquals(Option.isOption(Option.Some(1)), true)
- assertStrictEquals(Option.isOption(Option.None()), true)
- assertStrictEquals(Option.isOption({}), false)
+ test('isOption', () => {
+ expect(pipe(Option.Some(1), Option.isOption)).toBe(true)
+ expect(pipe(Option.None(), Option.isOption)).toBe(true)
+ expect(pipe({}, Option.isOption)).toBe(false)
})
describe('isNone', () => {
test('on Option static method ', () => {
- assertStrictEquals(Option.isNone(Option.None()), true)
- assertStrictEquals(Option.isNone(Option.Some(1)), false)
+ expect(pipe(Option.None(), Option.isNone)).toBe(true)
+ expect(pipe(Option.Some(1), Option.isNone)).toBe(false)
})
test('on Option instances: Some and None ', () => {
- assertStrictEquals(Option.fromNullable(42).isNone(), false)
- assertStrictEquals(Option.fromNullable(undefined).isNone(), true)
+ expect(Option.fromNullable(42).isNone()).toBe(false)
+ expect(Option.fromNullable(undefined).isNone()).toBe(true)
})
})
+ test('isEmpty', () => {
+ expect(Option.None().isEmpty()).toBe(true)
+ expect(Option.Some('foo').isEmpty()).toBe(false)
+ })
+
describe('isSome', () => {
test('on Option static method ', () => {
- assertStrictEquals(Option.isSome(Option.None()), false)
- assertStrictEquals(Option.isSome(Option.Some(1)), true)
+ expect(pipe(Option.None(), Option.isSome)).toBe(false)
+ expect(pipe(Option.Some(1), Option.isSome)).toEqual(true)
})
test('on Option instances: Some and None ', () => {
- assertStrictEquals(Option.fromNullable(null).isSome(), false)
- assertStrictEquals(Option.fromNullable(0).isSome(), true)
+ expect(Option.fromNullable(null).isSome()).toBe(false)
+ expect(Option.fromNullable(0).isSome()).toBe(true)
})
})
})
describe('serialize', () => {
- it('toStringTag', () => {
- assertStrictEquals(
- String(Option.Some(1)),
+ test('toStringTag', () => {
+ expect(String(Option.Some(1))).toBe(
JSON.stringify(
{
_id: 'Option',
@@ -87,8 +280,7 @@ describe('Option', () => {
2,
),
)
- assertStrictEquals(
- String(Option.None()),
+ expect(String(Option.None())).toBe(
JSON.stringify(
{
_id: 'Option',
@@ -102,28 +294,28 @@ describe('Option', () => {
})
describe('Equal', () => {
- it('None ', () => {
- assertStrictEquals(Option.None().equals(Option.None()), true)
- assertStrictEquals(Option.None().equals(Option.Some('a')), false)
+ test('None ', () => {
+ expect(Option.None().equals(Option.None())).toBe(true)
+ expect(Option.None().equals(Option.Some('a'))).toBe(false)
})
- it('Some', () => {
- assertStrictEquals(Option.Some('a').equals(Option.None()), false)
- assertStrictEquals(Option.Some(1).equals(Option.Some(2)), false)
- assertStrictEquals(Option.Some(1).equals(Option.Some(1)), true)
+ test('Some', () => {
+ expect(Option.Some('a').equals(Option.None())).toBe(false)
+ expect(Option.Some(1).equals(Option.Some(2))).toBe(false)
+ expect(Option.Some(1).equals(Option.Some(1))).toBe(true)
- assertStrictEquals(Option.Some([]).equals(Option.Some([])), false)
- assertStrictEquals(Option.Some([]).equals(Option.Some([])), false)
+ expect(Option.Some([]).equals(Option.Some([]))).toBe(false)
+ expect(Option.Some([]).equals(Option.Some([]))).toBe(false)
const arr = [1, 2, 3]
- assertStrictEquals(Option.Some(arr).equals(Option.Some(arr)), true)
- assertStrictEquals(Option.Some(arr).equals(Option.Some([1, 2, 3])), false)
+ expect(Option.Some(arr).equals(Option.Some(arr))).toBe(true)
+ expect(Option.Some(arr).equals(Option.Some([1, 2, 3]))).toBe(false)
})
- it('should use the custom comparison predicate strategy', () => {
- assertStrictEquals(Option.Some([]).equals(Option.Some([]), equal), true)
- assertStrictEquals(Option.Some([1, 2, 3]).equals(Option.Some([1, 2, 3]), equal), true)
- assertStrictEquals(
+ test('should use the custom comparison predicate strategy', () => {
+ expect(Option.Some([]).equals(Option.Some([]), equal)).toBe(true)
+ expect(Option.Some([1, 2, 3]).equals(Option.Some([1, 2, 3]), equal)).toBe(true)
+ expect(
Option.Some({
foo: {
bar: {
@@ -140,35 +332,35 @@ describe('Option', () => {
}),
equal,
),
- true,
- )
+ ).toBe(true)
})
})
describe('Inspectable', () => {
const some = Option.Some(1)
const none = Option.None()
- it('toStringTag', () => {
- assertStrictEquals(some[Symbol.toStringTag], 'Option.Some')
- assertStrictEquals(none[Symbol.toStringTag], 'Option.None')
+
+ test('toStringTag', () => {
+ expect(some[Symbol.toStringTag]).toBe('Option.Some')
+ expect(none[Symbol.toStringTag]).toBe('Option.None')
})
- it('toJSON', () => {
- assertEquals(some.toJSON(), {
+ test('toJSON', () => {
+ expect(some.toJSON()).toEqual({
_id: 'Option',
_tag: 'Some',
value: 1,
})
- assertEquals(none.toJSON(), {
+ expect(none.toJSON()).toEqual({
_id: 'Option',
_tag: 'None',
})
})
- it('toString', () => {
- assertStrictEquals(some.toString(), JSON.stringify(some.toJSON(), null, 2))
- assertStrictEquals(none.toString(), JSON.stringify(none.toJSON(), null, 2))
+ test('toString', () => {
+ expect(some.toString()).toBe(JSON.stringify(some.toJSON(), null, 2))
+ expect(none.toString()).toBe(JSON.stringify(none.toJSON(), null, 2))
})
})
})
diff --git a/src/option/option.ts b/src/option/option.ts
index 8831505..aa48f76 100644
--- a/src/option/option.ts
+++ b/src/option/option.ts
@@ -1,47 +1,5 @@
-/**
- * # Option Module
- * The Option Module is inspired by the Scala 3 Option type. This module primarily solves one of the common problems in programming - avoiding null and undefined values.
- *
- * The Option type is a container that encapsulates an optional value, i.e., it stores some value or none. It's a safer alternative to using null or undefined. By using Option, you can avoid null reference errors. The main advantage of using an `Option` type is that you're explicitly dealing with something that may or may not be there.
- *
- * ## How it works
- * This module exports a base abstract class `Option` and two concrete subclasses `Some` and `None`. An `Option` object encapsulates a value that may or may not be present. A `Some` object represents an `Option` that contains a value, and `None` represents an `Option` that has no value.
- *
- * ## How to use it
- * When you have a value that you want to lift into a boxed `Option`, you create an object of type `Some`, as follows:
- *
- * ```ts
- * import { Option } from './option.ts'
- *
- * const value = Option.Some("Hello, world!");
- * ```
- *
- * If there isn't a value to lift, you create a `None` object, as follows:
- *
- * ```ts
- * import { Option } from './option.ts'
- *
- * const value = Option.None();
- * ```
- *
- * To check the contents of an `Option`, use the `isSome` and `isNone` methods and extract the value when it is `Some`:
- *
- * ```ts
- * import { Option } from './option.ts'
- *
- * const value = Option.None();
- *
- * if (value.isSome()) {
- * console.log(value.value);
- * } else {
- * console.log("Value is None");
- * }
- * ```
- *
- * @module
- */
-
import type { Equals } from '../internal/equals.ts'
+import type * as F from '../internal/function.ts'
import type { Inspectable } from '../internal/inspectable.ts'
function format(x: unknown): string {
@@ -54,12 +12,12 @@ function format(x: unknown): string {
* The most idiomatic way to use an Option instance is to treat it as monad and use `map`,`flatMap`,` filter`, or `foreach`:
* These are useful methods that exist for both Some and None:
* -[ ] `isDefined` : True if not empty
- * -[ ] `isEmpty` : True if empty
+ * - {@linkcode Option#isEmpty} : True if empty
* -[ ] `nonEmpty`: True if not empty
* -[ ] `orElse`: Evaluate and return alternate optional value if empty
* -[ ] `getOrElse`: Evaluate and return alternate value if empty
- * -[ ] `get`: Return value, throw exception if empty
- * -[ ] `fold`: Apply function on optional value, return default if empty
+ * - {@linkcode Option#get} : Return value, throw exception if empty
+ * - {@linkcode Option#fold}: Apply function on optional value, return default if empty
* -[ ] `map`: Apply a function on the optional value
* -[ ] `flatMap`: Same as map but function must return an optional value
* -[ ] `foreach`: Apply a procedure on option value
@@ -74,8 +32,33 @@ function format(x: unknown): string {
* -[ ] `unzip3`: Split an optional triple to three optional values
* -[ ] `toList`: Unary list of optional value, otherwise the empty list
* A less-idiomatic way to use Option values is via pattern matching method `match`:
+ *
+ * ```ts
+ * import { assertStrictEquals } from 'jsr:@std/assert'
+ * import { pipe } from '../internal/function.ts'
+ * import { Option } from './option.ts'
+ *
+ * assertStrictEquals(
+ * pipe(
+ * Option.fromNullable(undefined),
+ * Option.match({
+ * onNone: () => 'a none',
+ * onSome: a => `a some containing ${a}`,
+ * }),
+ * ),
+ * 'a none',
+ * )
+ *
+ * assertStrictEquals(
+ * Option.fromNullable(1).match({
+ * onNone: () => 'a none',
+ * onSome: a => `a some containing ${a}`,
+ * }),
+ * 'a some containing 1',
+ * )
+ * ```
*/
-export abstract class Option implements Inspectable, Equals {
+export abstract class Option implements Inspectable, Equals {
/**
* The discriminant property that identifies the type of the `Option` instance.
*/
@@ -93,12 +76,14 @@ export abstract class Option implements Inspectable, Equals {
*/
protected constructor() {
if (new.target === Option) {
- throw new Error('Option is not meant to be instantiated directly')
+ throw new Error(
+ "Option is not meant to be instantiated directly; instantiate instead Option's derived classes (Some, None)",
+ )
}
}
/**
- * Overloads default {@linkcode Object#[Symbol#toStringTag]} getter allowing Option to return a custom string
+ * Overloads default {@linkcode Object.[Symbol.toStringTag]} getter allowing Option to return a custom string
*/
public get [Symbol.toStringTag](): string {
return `Option.${this._tag}`
@@ -116,12 +101,12 @@ export abstract class Option implements Inspectable, Equals {
return {
_id: 'Option',
_tag: this._tag,
- ...(this.isSome() ? { value: this.value } : {}),
+ ...(this.isSome() ? { value: this.get() } : {}),
}
}
/**
- * Overloads default {@linkcode Object#prototype#toString} method allowing Option to return a custom string representation of the boxed value
+ * Overloads default {@linkcode Object.prototype.toString} method allowing Option to return a custom string representation of the boxed value
* @override
*/
public toString(this: Option.Type): string {
@@ -133,8 +118,8 @@ export abstract class Option implements Inspectable, Equals {
*
* @category constructors
*/
- public static None(): Option.Type {
- return None.getSingletonInstance()
+ public static None(): Option.Type {
+ return None.getSingletonInstance()
}
/**
@@ -147,25 +132,116 @@ export abstract class Option implements Inspectable, Equals {
return new Some(value)
}
+ /**
+ * Returns a new function that takes an Option and returns the result of applying `f` to Option's value if the Option is nonempty. Otherwise, evaluates expression `ifEmpty`.
+ * This is equivalent to:
+ * ```ts
+ * import { Option } from './option.ts'
+ *
+ * declare const option: Option.Type
+ * declare const f: (a: A) => B
+ * declare const ifEmpty: () => B
+ *
+ * option.match({
+ * onSome: (x) => f(x),
+ * onNone: () => ifEmpty(),
+ * })
+ * ```
+ *
+ * This is also equivalent to:
+ * ```ts no-eval no-assert
+ * import { Option } from './option.ts'
+ *
+ * declare const option: Option.Type
+ * declare const f: (a: A) => B
+ * declare const ifEmpty: () => B
+ *
+ * option.map(f).getOrElse(ifEmpty())
+ * ```
+ *
+ * @param ifEmpty - The expression to evaluate if empty.
+ * @param f - The function to apply if nonempty.
+ * @returns a function that takes an Option and returns the result of applying `f` to this Option's value if the Option is nonempty. Otherwise, evaluates expression `ifEmpty`.
+ *
+ * @remarks
+ * This is a curried function, so it can be partially applied.
+ * It is also known as `reduce` in other languages.
+ *
+ * @example
+ * ( ( ) -> B, (A) -> B ) -> Option.Type -> B
+ * ```ts
+ * import { assertStrictEquals } from 'jsr:@std/assert'
+ * import type * as F from '../internal/function.ts'
+ *
+ * import { Option } from './option.ts'
+ *
+ * // Define a function to execute if the option holds a value
+ * const increment: (x: number) => number = a => a + 1
+ * // Define function to be evaluated if the Option is empty
+ * const ifEmpty:F.Lazy = () => 0
+ *
+ * // Define curry function
+ * const incrementOrZero = Option.fold(ifEmpty, increment)
+ *
+ * const two = incrementOrZero(Option.Some(1))
+ * // ^? number
+ * assertStrictEquals(two, 2)
+ *
+ * const zero: number = incrementOrZero(Option.None())
+ * // ^? number
+ * assertStrictEquals(zero, 0)
+ * ```
+ *
+ * @example
+ * ( ( ) -> B, (A) -> C ) -> Option.Type -> B | C
+ * ```ts
+ * import { assertStrictEquals } from 'jsr:@std/assert'
+ * import type * as F from '../internal/function.ts'
+ *
+ * import { Option } from './option.ts'
+ *
+ * const fold = Option.fold(() => null, (a: number) => a + 1)
+ *
+ * const two: number | null = fold(Option.Some(1))
+ * // ^? number | null
+ * assertStrictEquals(two, 2)
+ *
+ * const numberOrNull: number | null = fold(Option.None())
+ * // ^? number | null
+ * assertStrictEquals(numberOrNull, null)
+ * ```
+ * @see {Option.match}
+ * @category scala3-api
+ */
+ public static fold(
+ ifEmpty: F.Lazy,
+ f: (a: A) => C,
+ ): (self: Option.Type) => B | C {
+ return (self) => (self.isEmpty() ? ifEmpty() : f(self.get()))
+ }
+
/**
* Constructs a new `Option` from a nullable type.
- * @param value - An nullable value
- * @returns An `Option` that is {@linkcode class None} if the value is `null` or `undefined`, otherwise a {@linkcode Some(1)} containing the value.
+ * @param nullableValue - An nullable nullableValue
+ * @returns An `Option` that is {@linkcode class None} if the nullableValue is `null` or `undefined`, otherwise a {@linkcode Some(1)} containing the nullableValue.
*
* @example
* ```ts
* import { assertStrictEquals } from 'jsr:@std/assert'
* import { Option } from './option.ts'
*
- * assertStrictEquals(Option.fromNullable(undefined), Option.None())
- * assertStrictEquals(Option.fromNullable(null), Option.None())
- * assertStrictEquals(Option.fromNullable(1), Option.Some(1))
+ * assertStrictEquals(Option.fromNullable(undefined), Option.None()) // None | Option.Some
+ * assertStrictEquals(Option.fromNullable(undefined as (undefined | string)), Option.None()) // None | Option.Some
+ * assertStrictEquals(Option.fromNullable(null), Option.None()) // None | Option.Some
+ * assertStrictEquals(Option.fromNullable(1), Option.Some(1)) // None | Option.Some
* ```
*
- * @category Constructors
+ * @category constructors
*/
- public static fromNullable(value: T): Option.Type> {
- return value === undefined || value === null ? None.getSingletonInstance() : new Some(value)
+ public static fromNullable(nullableValue: T): Option.Type> {
+ return nullableValue === undefined || nullableValue === null
+ ? None.getSingletonInstance>()
+ : Option.Some(nullableValue as NonNullable)
}
/**
@@ -183,7 +259,7 @@ export abstract class Option implements Inspectable, Equals {
* ```
* @category type-guards
*/
- public static isNone(self: Option.Type): self is None {
+ public static isNone(self: Option.Type): self is None {
return self instanceof None
}
@@ -226,6 +302,47 @@ export abstract class Option implements Inspectable, Equals {
return self instanceof Some
}
+ /**
+ * Curried pattern matching for `Option` instances.
+ * Given a pattern matching object, it will return a function that will match the `Option` instance against the pattern.
+ *
+ * @param cases - The pattern matching object
+ * @param cases.onNone - The lazy value to be returned if the `Option` is `None`;
+ * @param cases.onSome - The function to be called if the `Option` is `Some`, it will be passed the `Option`'s value and its result will be returned
+ * @returns A function that will match the `Option` instance against the pattern.
+ *
+ * @example
+ * ```ts
+ * import { assertStrictEquals } from 'jsr:@std/assert'
+ * import { Option } from './option.ts'
+ * import { pipe } from '../internal/function.ts'
+ *
+ * assertStrictEquals(
+ * pipe(
+ * Option.Some(1),
+ * Option.match({ onNone: () => 'a none', onSome: (a) => `a some containing ${a}` })
+ * ),
+ * 'a some containing 1'
+ * )
+ *
+ * assertStrictEquals(
+ * pipe(
+ * Option.None(),
+ * Option.match({ onNone: () => 'a none', onSome: (a) => `a some containing ${a}` })
+ * ),
+ * 'a none'
+ * )
+ * ```
+ * @see {Option.fold}
+ * @category pattern matching
+ */
+ public static match(cases: {
+ readonly onNone: F.Lazy
+ readonly onSome: (a: A) => C
+ }): (self: Option.Type) => B | C {
+ return Option.fold(cases.onNone, cases.onSome)
+ }
+
/**
* Implements the {@linkcode Equals} interface, providing a way to compare two this Option instance with another unknown value that may be an Option or not.
*
@@ -236,6 +353,7 @@ export abstract class Option implements Inspectable, Equals {
* In its unary form, it uses referential equality (employing the Object.is algorithm). This behavior can be overridden by providing a custom predicate strategy as second argument.
*
* @example
+ * Using the default referential equality strategy on primitive types:
* ```ts
* import { Option } from './option.ts'
* import { assertStrictEquals, equal } from 'jsr:@std/assert'
@@ -247,16 +365,33 @@ export abstract class Option implements Inspectable, Equals {
* assertStrictEquals(some1.equals(some1), true)
* assertStrictEquals(some1.equals(none), false)
* assertStrictEquals(none.equals(none), true)
+ * ```
+ *
+ * @example
+ * Using the default referential equality strategy on non-primitive types:
+ * ```ts
+ * import { Option } from './option.ts'
+ * import { assertStrictEquals, equal } from 'jsr:@std/assert'
+ *
+ * const some1 = Option.Some(1)
+ * const none = Option.None()
*
* // equality derive types
* assertStrictEquals(some1.equals(Option.Some(1)), true)
* const someRecord = Option.Some({ foo: 'bar' })
* assertStrictEquals(someRecord.equals(someRecord), true)
* assertStrictEquals(someRecord.equals(Option.Some({ foo: 'bar' })), false)
+ * ```
+ *
+ * @example
+ * Using a custom predicate strategy:
+ * ```ts
+ * import { Option } from './option.ts'
+ * import { assertStrictEquals, equal } from 'jsr:@std/assert'
*
* // equality with custom predicate strategy
* assertStrictEquals(
- * someRecord.equals(
+ * Option.Some({ foo: 'bar' }).equals(
* Option.Some({ foo: 'bar' }),
* equal, // a custom deep equality strategy
* ),
@@ -272,17 +407,84 @@ export abstract class Option implements Inspectable, Equals {
return this.isSome()
? Option.isOption(that) &&
Option.isSome(that) &&
- predicateStrategy(this.value, that.value as That)
+ predicateStrategy(this.get(), that.get() as That)
: Option.isOption(that) && Option.isNone(that)
}
+ /**
+ * Returns the result of applying f to this Option's value if the Option is
+ * nonempty. Otherwise, evaluates expression ifEmpty.
+ *
+ * @param ifEmpty - The expression to evaluate if empty.
+ * @param f - The function to apply if nonempty.
+ * @returns The result of applying `f` to this Option's value if the Option is nonempty. Otherwise, evaluates expression `ifEmpty`.
+ *
+ * @example
+ * ```ts
+ * import { Option } from './option.ts'
+ * import { assertStrictEquals } from 'jsr:@std/assert'
+ *
+ * assertStrictEquals(Option.Some(1).fold(() => 0, (a) => a + 1), 2)
+ * assertStrictEquals(Option.Some(1).fold(() => 0, (a) => a + 1), 0)
+ * ```
+ * @category scala3-api
+ * @see {Option.fold}
+ */
+ public fold(
+ this: Option.Type,
+ ifEmpty: F.Lazy,
+ f: (a: NoInfer) => C,
+ ): B | C {
+ return Option.fold(ifEmpty, f)(this)
+ }
+
+ /**
+ * Returns the option's value.
+ * @throws Error - if the option is empty
+ * @remarks The option must be nonempty.
+ */
+ public abstract get(): A
+
+ /**
+ * Returns the option's value if the option is nonempty, otherwise return the result of evaluating default.
+ *
+ * This is equivalent to the following pattern match:
+ * ```ts no-assert
+ * import { Option } from './option.ts'
+ *
+ * declare const option: Option.Type
+ * declare const defaultValue: () => B
+ *
+ * option.match({
+ * onNone: () => defaultValue(),
+ * onSome: (a) => a
+ * })
+ * ```
+ *
+ * @param defaultValue - T the default expression. It will be evaluated if the option is empty.
+ */
+ public getOrElse(this: Option.Type, defaultValue: F.Lazy): A | B {
+ return this.isEmpty() ? defaultValue() : this.get()
+ }
+
+ /**
+ * Type guard that returns `true` if the option is None, `false` otherwise.
+ *
+ * @returns `true` if the option is None, `false` otherwise.
+ * @alias isNone
+ * @category type-guards, scala3-api
+ */
+ isEmpty(this: Option.Type): this is None {
+ return this.isNone()
+ }
+
/**
* Type guard that checks if the `Option` instance is a {@linkcode None}.
* @returns `true` if the `Option` instance is a {@linkcode None}, `false` otherwise.
*
* @category type-guards
*/
- public isNone(this: Option.Type): this is None {
+ public isNone(this: Option.Type): this is None {
return Option.isNone(this)
}
@@ -294,6 +496,52 @@ export abstract class Option implements Inspectable, Equals {
public isSome(this: Option.Type): this is Some {
return Option.isSome(this)
}
+
+ /**
+ * Pattern matches the value of the Option.
+ *
+ * @this {Some | None} - The `Option` instance to match.
+ * @param cases - The pattern matching object containing callbacks for different case branches:
+ * @param cases.onNone - The lazy value to be returned if the `Option` is `None`;
+ * @param cases.onSome - The function to be called if the `Option` is `Some`, it will be passed the `Option`'s value and its result will be returned
+ * @returns The value returned by the corresponding callback from the cases object of type `B` or `C`.
+ *
+ * @example
+ * ```ts
+ * import { assertStrictEquals } from 'jsr:@std/assert'
+ * import { Option } from './option.ts'
+ *
+ * assertStrictEquals(
+ * Option.fromNullable(1)
+ * .match({
+ * onNone: () => 'a none',
+ * onSome: (a) => `a some containing ${a}`
+ * }),
+ * 'a some containing 1'
+ * )
+ *
+ * assertStrictEquals(
+ * Option.fromNullable(null)
+ * .match({
+ * onNone: () => 'a none',
+ * onSome: (a) => `a some containing ${a}`
+ * }),
+ * 'a none'
+ * )
+ * ```
+ * @see {Option.fold}
+ * @see {Option.match}
+ * @category pattern-matching
+ */
+ public match(
+ this: Option.Type,
+ cases: {
+ readonly onNone: F.Lazy
+ readonly onSome: (a: A) => C
+ },
+ ): B | C {
+ return Option.match(cases)(this)
+ }
}
/**
@@ -315,27 +563,52 @@ export declare namespace Option {
*
* @category type-level
*/
- export type Type = None | Some
+ export type Type = None | Some
+
+ /**
+ * Type utility to extract the type of the value from an Option.
+ *
+ * @example
+ * ```ts
+ * import { Option } from './option.ts'
+ *
+ * const aNumber: number = 42
+ * const someOfNumber: Option.Type = Option.Some(aNumber)
+ * const test1: Option.Value = aNumber // ✅ no ts error!
+ * // ^? number
+ *
+ * // @ts-expect-error Type 'string' is not assignable to type 'number'.
+ * const test2: Option.Value = "42" // 💥ts error!
+ * ```
+ */
+ export type Value> = [T] extends [Option.Type] ? _A
+ : never
}
/**
* Case class representing the absence of a value.
* @class None
* @extends Option
- * @public
+ * @internal
*/
-class None extends Option {
- static #instance: undefined | None = undefined
-
+export class None extends Option {
+ static #instance: undefined | None = undefined
public readonly _tag = 'None' as const
+ /**
+ * @inheritDoc
+ */
+ public get(): never {
+ throw new Error('None.get')
+ }
+
/**
* Creates a new immutable `None` instance.
*
* @returns An instance of `None`
* @remaks
* We don't need multiple instances of `None` in memory, therefore we can save memory by using a singleton pattern.
- * Do not call this constructor directly. Instead, use {@linkcode None#getSingletonInstance}.
+ * Do not call this constructor directly. Instead, use {@linkcode None.getSingletonInstance}.
* @hideconstructor
*/
private constructor() {
@@ -347,15 +620,15 @@ class None extends Option {
* Returns the singleton instance of `None`.
*
* @returns The singleton instance of `None`.
- * @internal - use instead {@linkcode Option#None}
+ * @internal - use instead {@linkcode Option.None}
*
* @category constructors
*/
- public static getSingletonInstance(): None {
+ public static getSingletonInstance(): None {
if (!None.#instance) {
- None.#instance = new None()
+ None.#instance = new None()
}
- return None.#instance
+ return None.#instance as None
}
}
@@ -363,11 +636,18 @@ class None extends Option {
* Case class representing the presence of a value.
* @class Some
* @extends Option
- * @public
+ * @internal
*/
-class Some extends Option {
+export class Some extends Option {
public readonly _tag = 'Some' as const
+ /**
+ * @inheritDoc
+ */
+ public get(): A {
+ return this.#value
+ }
+
readonly #value: A
/**
@@ -375,7 +655,7 @@ class Some extends Option {
*
* @param value - The `value` to wrap.
* @returns An instance of `Some`
- * @remarks Do not call this constructor directly. Instead, use the static {@linkcode Option#Some}
+ * @remarks Do not call this constructor directly. Instead, use the static {@linkcode Option.Some}
* @hideconstructor
*/
public constructor(value: A) {
@@ -383,8 +663,4 @@ class Some extends Option {
this.#value = value
Object.freeze(this)
}
-
- public get value(): A {
- return this.#value
- }
}