initial commit
This commit is contained in:
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# compiled output
|
||||||
|
dist
|
||||||
|
tmp
|
||||||
|
out-tsc
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# environment
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# coverage
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# misc
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
testem.log
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
5
.prettierrc
Normal file
5
.prettierrc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
62
CLAUDE.md
Normal file
62
CLAUDE.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
## Build & Test Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build (dual ESM/CJS via tsup)
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# Test (Vitest runtime tests)
|
||||||
|
pnpm test
|
||||||
|
|
||||||
|
# Run a single test file
|
||||||
|
pnpm test -- src/option/spec/map.spec.ts
|
||||||
|
|
||||||
|
# Type-level tests (vitest typecheck, runs *.type-spec.ts files)
|
||||||
|
pnpm test:types
|
||||||
|
|
||||||
|
# Watch mode
|
||||||
|
pnpm test:watch
|
||||||
|
|
||||||
|
# Type-check without emitting
|
||||||
|
pnpm typecheck
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
pnpm lint
|
||||||
|
|
||||||
|
# Format
|
||||||
|
pnpm format
|
||||||
|
pnpm format:check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
`native-monad` is a monad library for TypeScript that uses native JS/TS idioms (Array.prototype.map, Array.isArray, Promise.all patterns). **Do not use Rust/FP naming conventions** (no unwrap, orElse, inspect, getOrElse, etc.). It provides three monadic types with class-based implementations:
|
||||||
|
|
||||||
|
- **Result** (`Ok<T> | Err<E>`) - success/failure. Factory functions: `ok(value)`, `err(error)`.
|
||||||
|
- **Option** (`Some<T> | None`) - presence/absence. Factory functions: `some(value)`, `none()`. `none()` returns a frozen singleton.
|
||||||
|
- **Query** (`OkSome<T> | OkNone | ErrNone<E>`) - three-state monad combining Result and Option (success-with-value, success-without-value, error). Factory functions: `okSome(value)`, `okNone()`, `errNone(error)`. `okNone()` returns a frozen singleton.
|
||||||
|
|
||||||
|
Each module (`src/result/`, `src/option/`, `src/query/`) follows the same pattern:
|
||||||
|
1. An **interface** defining the API contract (e.g. `ResultInterface<T, E>`)
|
||||||
|
2. **Class implementations** for each variant (e.g. `Ok<T>`, `Err<E>`)
|
||||||
|
3. A **union type** alias (e.g. `type Result<T, E> = Ok<T> | Err<E>`)
|
||||||
|
4. **Factory functions** as lowercase creators (e.g. `ok()`, `err()`)
|
||||||
|
5. A **namespace object** with static collection operations (`Result.all()`, `Result.from()`, `Result.partition()`, `Result.compact()`, etc.)
|
||||||
|
6. A **Promise namespace** wrapping collection operations for async usage (`ResultPromise`, `OptionPromise`, `QueryPromise`)
|
||||||
|
|
||||||
|
The three types are independent modules with no cross-type conversions. Users convert via `match()` if needed.
|
||||||
|
|
||||||
|
## Test Conventions
|
||||||
|
|
||||||
|
There are two parallel test systems:
|
||||||
|
- **`*.spec.ts`** - Runtime behavior tests using Vitest with Arrange/Act/Assert pattern
|
||||||
|
- **`*.type-spec.ts`** - Compile-time type tests using vitest typecheck with expect-type (`expectTypeOf(x).toEqualTypeOf<T>()`)
|
||||||
|
|
||||||
|
Tests are colocated in `spec/` directories next to the module they test, organized by operation (e.g. `map.spec.ts`, `filter.spec.ts`, `is.spec.ts`).
|
||||||
|
|
||||||
|
## Key Design Details
|
||||||
|
|
||||||
|
- **Falsy** is defined as only `null | undefined` in `src/util/index.ts`. Values like `0`, `false`, and `""` are **not** considered falsy. `Option.from()` and `Query.from()` use this definition.
|
||||||
|
- Type narrowing is central: `isOk()`/`isErr()` are type guards (`this is Ok<T>`) enabling direct `.value`/`.err` access after checking.
|
||||||
|
- The package ships dual ESM/CJS via tsup. Entry point: `src/index.ts`.
|
||||||
76
README.md
Normal file
76
README.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# native-monad
|
||||||
|
|
||||||
|
TypeScript monad library using native JS/TS idioms - Result, Option, and Query types.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install native-monad
|
||||||
|
```
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
### Result\<T, E\>
|
||||||
|
|
||||||
|
Success/failure type. Variants: `Ok<T>` | `Err<E>`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ok, err, Result } from 'native-monad';
|
||||||
|
|
||||||
|
const success = ok(42); // Ok<number>
|
||||||
|
const failure = err('oops'); // Err<string>
|
||||||
|
|
||||||
|
success.map(x => x * 2); // Ok(84)
|
||||||
|
failure.map(x => x * 2); // Err('oops')
|
||||||
|
|
||||||
|
success.match(
|
||||||
|
value => `got ${value}`,
|
||||||
|
error => `failed: ${error}`,
|
||||||
|
); // 'got 42'
|
||||||
|
|
||||||
|
// Collection operations
|
||||||
|
Result.all([ok(1), ok(2), ok(3)]); // Ok([1, 2, 3])
|
||||||
|
Result.all([ok(1), err('x')]); // Err('x')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option\<T\>
|
||||||
|
|
||||||
|
Presence/absence type. Variants: `Some<T>` | `None`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { some, none, Option } from 'native-monad';
|
||||||
|
|
||||||
|
const present = some('hello'); // Some<string>
|
||||||
|
const absent = none(); // None
|
||||||
|
|
||||||
|
present.map(s => s.toUpperCase()); // Some('HELLO')
|
||||||
|
absent.map(s => s.toUpperCase()); // None
|
||||||
|
|
||||||
|
// Create from nullable values (only null/undefined are falsy)
|
||||||
|
Option.from(0); // Some(0)
|
||||||
|
Option.from(null); // None
|
||||||
|
Option.from(undefined); // None
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query\<T, E\>
|
||||||
|
|
||||||
|
Three-state monad combining Result and Option. Variants: `OkSome<T>` | `OkNone` | `ErrNone<E>`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { okSome, okNone, errNone } from 'native-monad';
|
||||||
|
|
||||||
|
const found = okSome(42); // success with value
|
||||||
|
const notFound = okNone(); // success without value
|
||||||
|
const failed = errNone('error'); // failure
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm build # Build (dual ESM/CJS)
|
||||||
|
pnpm test # Run Jest tests
|
||||||
|
pnpm test:types # Run vitest type tests
|
||||||
|
pnpm lint # ESLint
|
||||||
|
pnpm typecheck # tsc --noEmit
|
||||||
|
```
|
||||||
86
benchmarks/README.md
Normal file
86
benchmarks/README.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# native-monad benchmarks
|
||||||
|
|
||||||
|
Comprehensive benchmark suite comparing `native-monad` against popular TypeScript monad libraries.
|
||||||
|
|
||||||
|
## Competitor Libraries
|
||||||
|
|
||||||
|
| Library | Result type | Option type | Async |
|
||||||
|
| -------------- | ---------------- | ------------ | --------------- |
|
||||||
|
| **neverthrow** | `Result<T,E>` | — | `ResultAsync` |
|
||||||
|
| **ts-results-es** | `Result<T,E>` | `Option<T>` | — |
|
||||||
|
| **oxide.ts** | `Result<T,E>` | `Option<T>` | — |
|
||||||
|
| **fp-ts** | `Either<E,A>` | `Option<A>` | `TaskEither` |
|
||||||
|
| **true-myth** | `Result<T,E>` | `Maybe<T>` | — |
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
The parent project must be built before running benchmarks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd .. && pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
This happens automatically via the `prebench` script when running `pnpm bench`.
|
||||||
|
|
||||||
|
## Running Benchmarks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies (first time only)
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Run all benchmarks
|
||||||
|
pnpm bench
|
||||||
|
|
||||||
|
# Run by type
|
||||||
|
pnpm bench:result
|
||||||
|
pnpm bench:option
|
||||||
|
pnpm bench:query
|
||||||
|
pnpm bench:scenarios
|
||||||
|
|
||||||
|
# Run a single file
|
||||||
|
npx vitest bench result/construction.bench.ts --run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reading Output
|
||||||
|
|
||||||
|
Vitest bench outputs a table per `describe` group:
|
||||||
|
|
||||||
|
| Column | Meaning |
|
||||||
|
| ------ | ------------------------------------------ |
|
||||||
|
| `hz` | Operations per second |
|
||||||
|
| `min` | Fastest single iteration (ms) |
|
||||||
|
| `max` | Slowest single iteration (ms) |
|
||||||
|
| `mean` | Average iteration time (ms) |
|
||||||
|
| `p75` | 75th percentile iteration time (ms) |
|
||||||
|
| `p99` | 99th percentile iteration time (ms) |
|
||||||
|
| `rme` | Relative margin of error (%) |
|
||||||
|
|
||||||
|
After all groups, a **Summary** section shows the fastest library per group and relative slowdown factors.
|
||||||
|
|
||||||
|
## Benchmark Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
result/ — Result/Either type benchmarks
|
||||||
|
option/ — Option/Maybe type benchmarks
|
||||||
|
query/ — Query (three-state) benchmarks + nested equivalents
|
||||||
|
scenarios/ — Real-world patterns (parsing, validation, error propagation)
|
||||||
|
helpers/ — Shared config, data generators, imports
|
||||||
|
```
|
||||||
|
|
||||||
|
Each benchmark file covers a specific operation category:
|
||||||
|
- `construction.bench.ts` — Creating Ok/Err/Some/None instances
|
||||||
|
- `instance-methods.bench.ts` — map, flatMap, mapErr, match, filter
|
||||||
|
- `type-guards.bench.ts` — isOk/isErr/isSome/isNone
|
||||||
|
- `conversion.bench.ts` — toNullable, toUndefined, toString
|
||||||
|
- `collection.bench.ts` — all, partition, compact, some, every
|
||||||
|
- `chaining.bench.ts` — Multi-step pipelines
|
||||||
|
- `from-wrapping.bench.ts` — Try-catch wrapping
|
||||||
|
- `async.bench.ts` — Promise-based operations
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
- **Microbenchmarks** measure isolated operations. Real-world performance depends on usage patterns, GC pressure, and JIT behavior in context.
|
||||||
|
- **Async benchmarks** have higher variance due to event loop scheduling. Results use extended warmup/time to compensate.
|
||||||
|
- **fp-ts** uses a fundamentally different paradigm (`pipe` + standalone functions vs class methods). The benchmarks use each library's idiomatic API for fair comparison.
|
||||||
|
- **`bench.skip`** appears when a library lacks an equivalent operation — no fabricated workarounds.
|
||||||
|
- Collection benchmarks test at sizes 100 and 10K to show scaling behavior.
|
||||||
25
benchmarks/helpers/bench-options.ts
Normal file
25
benchmarks/helpers/bench-options.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { BenchOptions } from 'vitest';
|
||||||
|
|
||||||
|
/** Standard options for most benchmarks — 500ms run time, 100 min iterations */
|
||||||
|
export const STANDARD: BenchOptions = {
|
||||||
|
time: 500,
|
||||||
|
iterations: 100,
|
||||||
|
warmupTime: 100,
|
||||||
|
warmupIterations: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Extended options for async/expensive benchmarks — 1s run time */
|
||||||
|
export const EXTENDED: BenchOptions = {
|
||||||
|
time: 1000,
|
||||||
|
iterations: 25,
|
||||||
|
warmupTime: 250,
|
||||||
|
warmupIterations: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Quick options for allocation/memory pattern benchmarks */
|
||||||
|
export const QUICK: BenchOptions = {
|
||||||
|
time: 250,
|
||||||
|
iterations: 500,
|
||||||
|
warmupTime: 50,
|
||||||
|
warmupIterations: 10,
|
||||||
|
};
|
||||||
22
benchmarks/helpers/data-generators.ts
Normal file
22
benchmarks/helpers/data-generators.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export const SIZES = [100, 10_000] as const;
|
||||||
|
|
||||||
|
export function generateNumbers(size: number): number[] {
|
||||||
|
return Array.from({ length: size }, (_, i) => i);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateStrings(size: number): string[] {
|
||||||
|
return Array.from({ length: size }, (_, i) => `item-${i}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates indices that should be "error" slots, deterministically distributed.
|
||||||
|
* With errRatio=0.2 and size=10, returns indices [4, 9] (every 5th element).
|
||||||
|
*/
|
||||||
|
export function errorIndices(size: number, errRatio = 0.2): Set<number> {
|
||||||
|
const step = Math.max(1, Math.floor(1 / errRatio));
|
||||||
|
const indices = new Set<number>();
|
||||||
|
for (let i = step - 1; i < size; i += step) {
|
||||||
|
indices.add(i);
|
||||||
|
}
|
||||||
|
return indices;
|
||||||
|
}
|
||||||
53
benchmarks/helpers/imports.ts
Normal file
53
benchmarks/helpers/imports.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// native-monad
|
||||||
|
export {
|
||||||
|
ok,
|
||||||
|
err,
|
||||||
|
some,
|
||||||
|
none,
|
||||||
|
okSome,
|
||||||
|
okNone,
|
||||||
|
errNone,
|
||||||
|
Result,
|
||||||
|
Option,
|
||||||
|
Query,
|
||||||
|
ResultPromise,
|
||||||
|
OptionPromise,
|
||||||
|
QueryPromise,
|
||||||
|
} from 'native-monad';
|
||||||
|
|
||||||
|
// neverthrow
|
||||||
|
export {
|
||||||
|
ok as ntOk,
|
||||||
|
err as ntErr,
|
||||||
|
Result as NtResult,
|
||||||
|
ResultAsync as NtResultAsync,
|
||||||
|
fromThrowable as ntFromThrowable,
|
||||||
|
} from 'neverthrow';
|
||||||
|
|
||||||
|
// ts-results-es
|
||||||
|
export {
|
||||||
|
Ok as TsOk,
|
||||||
|
Err as TsErr,
|
||||||
|
Some as TsSome,
|
||||||
|
None as TsNone,
|
||||||
|
Result as TsResult,
|
||||||
|
Option as TsOption,
|
||||||
|
} from 'ts-results-es';
|
||||||
|
|
||||||
|
// oxide.ts
|
||||||
|
export {
|
||||||
|
Ok as OxOk,
|
||||||
|
Err as OxErr,
|
||||||
|
Some as OxSome,
|
||||||
|
None as OxNone,
|
||||||
|
match as oxMatch,
|
||||||
|
} from 'oxide.ts';
|
||||||
|
|
||||||
|
// fp-ts
|
||||||
|
export * as E from 'fp-ts/Either';
|
||||||
|
export * as O from 'fp-ts/Option';
|
||||||
|
export { pipe } from 'fp-ts/function';
|
||||||
|
|
||||||
|
// true-myth (static functions are named exports, not on default)
|
||||||
|
export * as TmResult from 'true-myth/result';
|
||||||
|
export * as TmMaybe from 'true-myth/maybe';
|
||||||
42
benchmarks/option/async.bench.ts
Normal file
42
benchmarks/option/async.bench.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { EXTENDED } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
some,
|
||||||
|
none,
|
||||||
|
OptionPromise,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// OptionPromise.from — returns value
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('OptionPromise.from — returns value', () => {
|
||||||
|
bench('native-monad', async () => {
|
||||||
|
await OptionPromise.from(() => Promise.resolve(42));
|
||||||
|
}, EXTENDED);
|
||||||
|
// No other library has an async Option wrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// OptionPromise.from — returns null
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('OptionPromise.from — returns null', () => {
|
||||||
|
bench('native-monad', async () => {
|
||||||
|
await OptionPromise.from(() => Promise.resolve(null));
|
||||||
|
}, EXTENDED);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// OptionPromise.all — all Some
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of [10, 100] as const) {
|
||||||
|
describe(`OptionPromise.all — ${size} items, all Some`, () => {
|
||||||
|
const promises = () => Array.from({ length: size }, (_, i) => Promise.resolve(some(i)));
|
||||||
|
|
||||||
|
bench('native-monad', async () => {
|
||||||
|
await OptionPromise.all(promises());
|
||||||
|
}, EXTENDED);
|
||||||
|
});
|
||||||
|
}
|
||||||
132
benchmarks/option/chaining.bench.ts
Normal file
132
benchmarks/option/chaining.bench.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
some,
|
||||||
|
none,
|
||||||
|
TsSome,
|
||||||
|
TsNone,
|
||||||
|
OxSome,
|
||||||
|
OxNone,
|
||||||
|
O,
|
||||||
|
pipe,
|
||||||
|
TmMaybe,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 5-step pipeline — Some path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option chaining — 5-step pipeline (all Some)', () => {
|
||||||
|
bench('native-monad', () => {
|
||||||
|
some(42)
|
||||||
|
.map(x => x * 2)
|
||||||
|
.flatMap(x => (x > 0 ? some(x) : none()))
|
||||||
|
.map(x => ({ value: x }))
|
||||||
|
.flatMap(x => some({ ...x, label: 'result' }))
|
||||||
|
.map(x => JSON.stringify(x));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('ts-results-es', () => {
|
||||||
|
TsSome(42)
|
||||||
|
.map(x => x * 2)
|
||||||
|
.andThen(x => (x > 0 ? TsSome(x) : TsNone))
|
||||||
|
.map(x => ({ value: x }))
|
||||||
|
.andThen(x => TsSome({ ...x, label: 'result' }))
|
||||||
|
.map(x => JSON.stringify(x));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('oxide.ts', () => {
|
||||||
|
OxSome(42)
|
||||||
|
.map(x => x * 2)
|
||||||
|
.andThen(x => (x > 0 ? OxSome(x) : OxNone))
|
||||||
|
.map(x => ({ value: x }))
|
||||||
|
.andThen(x => OxSome({ ...x, label: 'result' }))
|
||||||
|
.map(x => JSON.stringify(x));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts', () => {
|
||||||
|
pipe(
|
||||||
|
O.some(42),
|
||||||
|
O.map(x => x * 2),
|
||||||
|
O.flatMap(x => (x > 0 ? O.some(x) : O.none)),
|
||||||
|
O.map(x => ({ value: x })),
|
||||||
|
O.flatMap(x => O.some({ ...x, label: 'result' })),
|
||||||
|
O.map(x => JSON.stringify(x)),
|
||||||
|
);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('true-myth', () => {
|
||||||
|
TmMaybe.just(42)
|
||||||
|
.map(x => x * 2)
|
||||||
|
.andThen(x => (x > 0 ? TmMaybe.just(x) : TmMaybe.nothing()))
|
||||||
|
.map(x => ({ value: x }))
|
||||||
|
.andThen(x => TmMaybe.just({ ...x, label: 'result' }))
|
||||||
|
.map(x => JSON.stringify(x));
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// None short-circuit — 9 no-op maps
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option chaining — None at step 1, 9 no-op maps', () => {
|
||||||
|
const nmNone = none();
|
||||||
|
const tsNone = TsNone;
|
||||||
|
const oxNone = OxNone;
|
||||||
|
const fpNone = O.none;
|
||||||
|
const tmNone = TmMaybe.nothing<number>();
|
||||||
|
const fn = (x: number) => x + 1;
|
||||||
|
|
||||||
|
bench('native-monad', () => {
|
||||||
|
nmNone.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('ts-results-es', () => {
|
||||||
|
tsNone.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('oxide.ts', () => {
|
||||||
|
oxNone.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts', () => {
|
||||||
|
pipe(fpNone, O.map(fn), O.map(fn), O.map(fn), O.map(fn), O.map(fn), O.map(fn), O.map(fn), O.map(fn), O.map(fn));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('true-myth', () => {
|
||||||
|
tmNone.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 20-step deep map chain — Some path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option chaining — 20 maps on Some', () => {
|
||||||
|
const fn = (x: number) => x + 1;
|
||||||
|
|
||||||
|
bench('native-monad', () => {
|
||||||
|
let r = some(0);
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('ts-results-es', () => {
|
||||||
|
let r = TsSome(0);
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn) as any;
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('oxide.ts', () => {
|
||||||
|
let r = OxSome(0);
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn) as any;
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts', () => {
|
||||||
|
let r = O.some(0) as O.Option<number>;
|
||||||
|
for (let i = 0; i < 20; i++) r = pipe(r, O.map(fn));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('true-myth', () => {
|
||||||
|
let r = TmMaybe.just(0);
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn) as any;
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
144
benchmarks/option/collection.bench.ts
Normal file
144
benchmarks/option/collection.bench.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD, EXTENDED } from '../helpers/bench-options.js';
|
||||||
|
import { SIZES, errorIndices } from '../helpers/data-generators.js';
|
||||||
|
import {
|
||||||
|
some,
|
||||||
|
none,
|
||||||
|
Option,
|
||||||
|
TsSome,
|
||||||
|
TsNone,
|
||||||
|
TsOption,
|
||||||
|
OxSome,
|
||||||
|
OxNone,
|
||||||
|
O,
|
||||||
|
pipe,
|
||||||
|
TmMaybe,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
import * as RA from 'fp-ts/ReadonlyArray';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Option.all — all Some
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of SIZES) {
|
||||||
|
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||||
|
|
||||||
|
const nmAll = Array.from({ length: size }, (_, i) => some(i));
|
||||||
|
const tsAll = Array.from({ length: size }, (_, i) => TsSome(i));
|
||||||
|
const fpAll = Array.from({ length: size }, (_, i) => O.some(i));
|
||||||
|
const tmAll = Array.from({ length: size }, (_, i) => TmMaybe.just(i));
|
||||||
|
|
||||||
|
describe(`Option.all — ${size} items, all Some`, () => {
|
||||||
|
bench('native-monad', () => { Option.all(nmAll); }, opts);
|
||||||
|
bench('ts-results-es', () => { TsOption.all(...tsAll); }, opts);
|
||||||
|
bench('fp-ts', () => { pipe(fpAll, RA.sequence(O.Applicative)); }, opts);
|
||||||
|
bench('true-myth (manual)', () => {
|
||||||
|
const values: number[] = [];
|
||||||
|
for (const m of tmAll) {
|
||||||
|
if (m.isNothing) return TmMaybe.nothing();
|
||||||
|
values.push((m as { value: number }).value);
|
||||||
|
}
|
||||||
|
return TmMaybe.just(values);
|
||||||
|
}, opts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Option.all — 20% None (short-circuit)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of SIZES) {
|
||||||
|
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||||
|
const noneIdx = errorIndices(size, 0.2);
|
||||||
|
|
||||||
|
const nmMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
noneIdx.has(i) ? none() : some(i),
|
||||||
|
);
|
||||||
|
const tsMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
noneIdx.has(i) ? TsNone : TsSome(i),
|
||||||
|
);
|
||||||
|
const fpMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
noneIdx.has(i) ? O.none : O.some(i),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe(`Option.all — ${size} items, 20% None (short-circuit)`, () => {
|
||||||
|
bench('native-monad', () => { Option.all(nmMixed); }, opts);
|
||||||
|
bench('ts-results-es', () => { TsOption.all(...tsMixed); }, opts);
|
||||||
|
bench('fp-ts', () => { pipe(fpMixed, RA.sequence(O.Applicative)); }, opts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Option.partition
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of SIZES) {
|
||||||
|
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||||
|
const noneIdx = errorIndices(size, 0.2);
|
||||||
|
|
||||||
|
const nmMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
noneIdx.has(i) ? none() : some(i),
|
||||||
|
);
|
||||||
|
const fpMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
noneIdx.has(i) ? O.none : O.some(i),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe(`Option.partition — ${size} items`, () => {
|
||||||
|
bench('native-monad', () => { Option.partition(nmMixed); }, opts);
|
||||||
|
bench('fp-ts (manual)', () => {
|
||||||
|
const values: number[] = [];
|
||||||
|
let noneCount = 0;
|
||||||
|
for (const o of fpMixed) {
|
||||||
|
if (O.isSome(o)) values.push(o.value);
|
||||||
|
else noneCount++;
|
||||||
|
}
|
||||||
|
return { values, noneCount };
|
||||||
|
}, opts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Option.compact
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of SIZES) {
|
||||||
|
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||||
|
const noneIdx = errorIndices(size, 0.2);
|
||||||
|
|
||||||
|
const nmMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
noneIdx.has(i) ? none() : some(i),
|
||||||
|
);
|
||||||
|
const fpMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
noneIdx.has(i) ? O.none : O.some(i),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe(`Option.compact — ${size} items`, () => {
|
||||||
|
bench('native-monad', () => { Option.compact(nmMixed); }, opts);
|
||||||
|
bench('fp-ts', () => { pipe(fpMixed, RA.compact); }, opts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Option.some / Option.every
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of [100, 1_000] as const) {
|
||||||
|
const noneIdx = errorIndices(size, 0.2);
|
||||||
|
|
||||||
|
const nmMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
noneIdx.has(i) ? none() : some(i),
|
||||||
|
);
|
||||||
|
const fpMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
noneIdx.has(i) ? O.none : O.some(i),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe(`Option.some — ${size} items`, () => {
|
||||||
|
bench('native-monad', () => { Option.some(nmMixed); }, STANDARD);
|
||||||
|
bench('fp-ts (manual)', () => { fpMixed.some(O.isSome); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`Option.every — ${size} items`, () => {
|
||||||
|
bench('native-monad', () => { Option.every(nmMixed); }, STANDARD);
|
||||||
|
bench('fp-ts (manual)', () => { fpMixed.every(O.isSome); }, STANDARD);
|
||||||
|
});
|
||||||
|
}
|
||||||
47
benchmarks/option/construction.bench.ts
Normal file
47
benchmarks/option/construction.bench.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
some,
|
||||||
|
none,
|
||||||
|
TsSome,
|
||||||
|
TsNone,
|
||||||
|
OxSome,
|
||||||
|
OxNone,
|
||||||
|
O,
|
||||||
|
TmMaybe,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Some construction
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option construction — Some (primitive)', () => {
|
||||||
|
bench('native-monad', () => { some(42); }, STANDARD);
|
||||||
|
// neverthrow: no Option type
|
||||||
|
bench('ts-results-es', () => { TsSome(42); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { OxSome(42); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { O.some(42); }, STANDARD);
|
||||||
|
bench('true-myth', () => { TmMaybe.just(42); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Option construction — Some (object payload)', () => {
|
||||||
|
const value = { id: 1, name: 'Alice', roles: ['admin', 'user'] };
|
||||||
|
|
||||||
|
bench('native-monad', () => { some(value); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { TsSome(value); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { OxSome(value); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { O.some(value); }, STANDARD);
|
||||||
|
bench('true-myth', () => { TmMaybe.just(value); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// None construction / access
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option construction — None', () => {
|
||||||
|
bench('native-monad', () => { none(); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { TsNone; }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { OxNone; }, STANDARD);
|
||||||
|
bench('fp-ts', () => { O.none; }, STANDARD);
|
||||||
|
bench('true-myth', () => { TmMaybe.nothing(); }, STANDARD);
|
||||||
|
});
|
||||||
55
benchmarks/option/conversion.bench.ts
Normal file
55
benchmarks/option/conversion.bench.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
some,
|
||||||
|
none,
|
||||||
|
OxSome,
|
||||||
|
OxNone,
|
||||||
|
O,
|
||||||
|
pipe,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// toNullable — Some path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.toNullable — Some', () => {
|
||||||
|
const nm = some(42);
|
||||||
|
const ox = OxSome(42);
|
||||||
|
const fp = O.some(42);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.toNullable(); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.into(); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, O.toNullable); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// toNullable — None path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.toNullable — None', () => {
|
||||||
|
const nm = none();
|
||||||
|
const ox = OxNone;
|
||||||
|
const fp = O.none;
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.toNullable(); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.into(); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, O.toNullable); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// toString
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.toString — Some', () => {
|
||||||
|
const nm = some(42);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.toString(); }, STANDARD);
|
||||||
|
// Most other libraries don't have toString
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Option.toString — None', () => {
|
||||||
|
const nm = none();
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.toString(); }, STANDARD);
|
||||||
|
});
|
||||||
59
benchmarks/option/from-wrapping.bench.ts
Normal file
59
benchmarks/option/from-wrapping.bench.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
Option,
|
||||||
|
O,
|
||||||
|
TmMaybe,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Option.from — non-nullish value
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.from — non-nullish value', () => {
|
||||||
|
bench('native-monad', () => { Option.from(42); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { O.fromNullable(42); }, STANDARD);
|
||||||
|
bench('true-myth', () => { TmMaybe.of(42); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Option.from — null value
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.from — null', () => {
|
||||||
|
bench('native-monad', () => { Option.from(null); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { O.fromNullable(null); }, STANDARD);
|
||||||
|
bench('true-myth', () => { TmMaybe.of(null); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Option.from — undefined value
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.from — undefined', () => {
|
||||||
|
bench('native-monad', () => { Option.from(undefined); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { O.fromNullable(undefined); }, STANDARD);
|
||||||
|
bench('true-myth', () => { TmMaybe.of(undefined); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Option.from — function that returns value
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.from — function returning value', () => {
|
||||||
|
const fn = () => 42;
|
||||||
|
|
||||||
|
bench('native-monad', () => { Option.from(fn); }, STANDARD);
|
||||||
|
// fp-ts: no function overload for fromNullable
|
||||||
|
// true-myth: no function overload for of
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Option.from — function that returns null
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.from — function returning null', () => {
|
||||||
|
const fn = () => null;
|
||||||
|
|
||||||
|
bench('native-monad', () => { Option.from(fn); }, STANDARD);
|
||||||
|
});
|
||||||
151
benchmarks/option/instance-methods.bench.ts
Normal file
151
benchmarks/option/instance-methods.bench.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
some,
|
||||||
|
none,
|
||||||
|
TsSome,
|
||||||
|
TsNone,
|
||||||
|
OxSome,
|
||||||
|
OxNone,
|
||||||
|
oxMatch,
|
||||||
|
O,
|
||||||
|
pipe,
|
||||||
|
TmMaybe,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
const double = (x: number) => x * 2;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// map — Some path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.map — Some path', () => {
|
||||||
|
const nm = some(42);
|
||||||
|
const ts = TsSome(42);
|
||||||
|
const ox = OxSome(42);
|
||||||
|
const fp = O.some(42);
|
||||||
|
const tm = TmMaybe.just(42);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.map(double); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { ts.map(double); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.map(double); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, O.map(double)); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.map(double); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// map — None path (no-op)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.map — None path (no-op)', () => {
|
||||||
|
const nm = none();
|
||||||
|
const ts = TsNone;
|
||||||
|
const ox = OxNone;
|
||||||
|
const fp = O.none;
|
||||||
|
const tm = TmMaybe.nothing<number>();
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.map(double); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { ts.map(double); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.map(double); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, O.map(double)); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.map(double); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// flatMap — Some path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.flatMap — Some path', () => {
|
||||||
|
const nm = some(42);
|
||||||
|
const ts = TsSome(42);
|
||||||
|
const ox = OxSome(42);
|
||||||
|
const fp = O.some(42);
|
||||||
|
const tm = TmMaybe.just(42);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.flatMap(x => some(x * 2)); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { ts.andThen(x => TsSome(x * 2)); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.andThen(x => OxSome(x * 2)); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, O.flatMap(x => O.some(x * 2))); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.andThen(x => TmMaybe.just(x * 2)); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// flatMap — None path (no-op)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.flatMap — None path (no-op)', () => {
|
||||||
|
const nm = none();
|
||||||
|
const ts = TsNone;
|
||||||
|
const ox = OxNone;
|
||||||
|
const fp = O.none;
|
||||||
|
const tm = TmMaybe.nothing<number>();
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.flatMap(x => some(x * 2)); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { ts.andThen(x => TsSome(x * 2)); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.andThen(x => OxSome(x * 2)); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, O.flatMap(x => O.some(x * 2))); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.andThen(x => TmMaybe.just(x * 2)); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// match — Some path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.match — Some path', () => {
|
||||||
|
const nm = some(42);
|
||||||
|
const ox = OxSome(42);
|
||||||
|
const fp = O.some(42);
|
||||||
|
const tm = TmMaybe.just(42);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.match(x => x * 2, () => 0); }, STANDARD);
|
||||||
|
// ts-results-es: no match
|
||||||
|
bench('oxide.ts', () => { oxMatch(ox, { Some: (x: number) => x * 2, None: () => 0 }); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, O.match(() => 0, x => x * 2)); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.match({ Just: x => x * 2, Nothing: () => 0 }); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// match — None path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.match — None path', () => {
|
||||||
|
const nm = none();
|
||||||
|
const ox = OxNone;
|
||||||
|
const fp = O.none;
|
||||||
|
const tm = TmMaybe.nothing<number>();
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.match(x => x * 2, () => 0); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { oxMatch(ox, { Some: (x: number) => x * 2, None: () => 0 }); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, O.match(() => 0, x => x * 2)); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.match({ Just: x => x * 2, Nothing: () => 0 }); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// filter — Some path (predicate passes)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.filter — predicate passes', () => {
|
||||||
|
const nm = some(42);
|
||||||
|
const ox = OxSome(42);
|
||||||
|
const fp = O.some(42);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.filter(x => x > 0); }, STANDARD);
|
||||||
|
// ts-results-es: no filter
|
||||||
|
bench('oxide.ts', () => { ox.filter(x => x > 0); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, O.filter(x => x > 0)); }, STANDARD);
|
||||||
|
// true-myth: no filter on Maybe
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// filter — predicate fails
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.filter — predicate fails', () => {
|
||||||
|
const nm = some(-1);
|
||||||
|
const ox = OxSome(-1);
|
||||||
|
const fp = O.some(-1);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.filter(x => x > 0); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.filter(x => x > 0); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, O.filter(x => x > 0)); }, STANDARD);
|
||||||
|
});
|
||||||
66
benchmarks/option/type-guards.bench.ts
Normal file
66
benchmarks/option/type-guards.bench.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
some,
|
||||||
|
none,
|
||||||
|
TsSome,
|
||||||
|
TsNone,
|
||||||
|
OxSome,
|
||||||
|
OxNone,
|
||||||
|
O,
|
||||||
|
TmMaybe,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// isSome — on Some
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.isSome — on Some', () => {
|
||||||
|
const nm = some(42);
|
||||||
|
const ts = TsSome(42);
|
||||||
|
const ox = OxSome(42);
|
||||||
|
const fp = O.some(42);
|
||||||
|
const tm = TmMaybe.just(42);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.isSome(); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { ts.isSome(); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.isSome(); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { O.isSome(fp); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.isJust; }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// isSome — on None
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.isSome — on None', () => {
|
||||||
|
const nm = none();
|
||||||
|
const ts = TsNone;
|
||||||
|
const ox = OxNone;
|
||||||
|
const fp = O.none;
|
||||||
|
const tm = TmMaybe.nothing<number>();
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.isSome(); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { ts.isSome(); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.isSome(); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { O.isSome(fp); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.isJust; }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// isNone — on None
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Option.isNone — on None', () => {
|
||||||
|
const nm = none();
|
||||||
|
const ts = TsNone;
|
||||||
|
const ox = OxNone;
|
||||||
|
const fp = O.none;
|
||||||
|
const tm = TmMaybe.nothing<number>();
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.isNone(); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { ts.isNone(); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.isNone(); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { O.isNone(fp); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.isNothing; }, STANDARD);
|
||||||
|
});
|
||||||
25
benchmarks/package.json
Normal file
25
benchmarks/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "native-monad-benchmarks",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"prebench": "cd .. && pnpm build",
|
||||||
|
"bench": "vitest bench",
|
||||||
|
"bench:parallel": "vitest bench result/ > results/result.txt 2>&1 & vitest bench option/ > results/option.txt 2>&1 & vitest bench query/ > results/query.txt 2>&1 & vitest bench scenarios/ > results/scenarios.txt 2>&1 & wait && cat results/result.txt results/option.txt results/query.txt results/scenarios.txt",
|
||||||
|
"bench:result": "vitest bench result/",
|
||||||
|
"bench:option": "vitest bench option/",
|
||||||
|
"bench:query": "vitest bench query/",
|
||||||
|
"bench:scenarios": "vitest bench scenarios/"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"native-monad": "file:..",
|
||||||
|
"neverthrow": "^8.0.0",
|
||||||
|
"ts-results-es": "^4.0.0",
|
||||||
|
"oxide.ts": "^1.1.0",
|
||||||
|
"fp-ts": "^2.16.0",
|
||||||
|
"true-myth": "^7.0.0",
|
||||||
|
"vitest": "^3.2.4",
|
||||||
|
"typescript": "^5.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1046
benchmarks/pnpm-lock.yaml
generated
Normal file
1046
benchmarks/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
benchmarks/query/async.bench.ts
Normal file
42
benchmarks/query/async.bench.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { EXTENDED } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
okSome,
|
||||||
|
okNone,
|
||||||
|
QueryPromise,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// QueryPromise.from — returns value
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('QueryPromise.from — returns value', () => {
|
||||||
|
bench('native-monad', async () => {
|
||||||
|
await QueryPromise.from(() => Promise.resolve(42));
|
||||||
|
}, EXTENDED);
|
||||||
|
// No other library has a three-state async wrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// QueryPromise.from — returns null
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('QueryPromise.from — returns null', () => {
|
||||||
|
bench('native-monad', async () => {
|
||||||
|
await QueryPromise.from(() => Promise.resolve(null));
|
||||||
|
}, EXTENDED);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// QueryPromise.all — all OkSome
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of [10, 100] as const) {
|
||||||
|
describe(`QueryPromise.all — ${size} items, all OkSome`, () => {
|
||||||
|
const promises = () => Array.from({ length: size }, (_, i) => Promise.resolve(okSome(i)));
|
||||||
|
|
||||||
|
bench('native-monad', async () => {
|
||||||
|
await QueryPromise.all(promises());
|
||||||
|
}, EXTENDED);
|
||||||
|
});
|
||||||
|
}
|
||||||
128
benchmarks/query/chaining.bench.ts
Normal file
128
benchmarks/query/chaining.bench.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
okSome,
|
||||||
|
okNone,
|
||||||
|
errNone,
|
||||||
|
E,
|
||||||
|
O,
|
||||||
|
pipe,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 5-step pipeline — OkSome path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Query chaining — 5-step pipeline (all OkSome)', () => {
|
||||||
|
bench('native-monad', () => {
|
||||||
|
okSome(42)
|
||||||
|
.map(x => x * 2)
|
||||||
|
.flatMap(x => (x > 0 ? okSome(x) : okNone()))
|
||||||
|
.map(x => ({ value: x }))
|
||||||
|
.flatMap(x => okSome({ ...x, label: 'result' }))
|
||||||
|
.map(x => JSON.stringify(x));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts (nested)', () => {
|
||||||
|
pipe(
|
||||||
|
E.right(O.some(42)) as E.Either<string, O.Option<number>>,
|
||||||
|
E.map(O.map(x => x * 2)),
|
||||||
|
E.flatMap(inner =>
|
||||||
|
pipe(
|
||||||
|
inner,
|
||||||
|
O.match(
|
||||||
|
() => E.right(O.none) as E.Either<string, O.Option<number>>,
|
||||||
|
v => (v > 0 ? E.right(O.some(v)) : E.right(O.none)) as E.Either<string, O.Option<number>>,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
E.map(O.map(x => ({ value: x }))),
|
||||||
|
E.flatMap(inner =>
|
||||||
|
pipe(
|
||||||
|
inner,
|
||||||
|
O.match(
|
||||||
|
() => E.right(O.none) as E.Either<string, O.Option<{ value: number; label: string }>>,
|
||||||
|
v => E.right(O.some({ ...v, label: 'result' })),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
E.map(O.map(x => JSON.stringify(x))),
|
||||||
|
);
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ErrNone short-circuit — 9 no-op maps
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Query chaining — ErrNone at step 1, 9 no-op maps', () => {
|
||||||
|
const nm = errNone('fail');
|
||||||
|
const fp = E.left('fail') as E.Either<string, O.Option<number>>;
|
||||||
|
const fn = (x: number) => x + 1;
|
||||||
|
|
||||||
|
bench('native-monad', () => {
|
||||||
|
nm.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts (nested)', () => {
|
||||||
|
pipe(
|
||||||
|
fp,
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
);
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// OkNone short-circuit — 9 no-op maps
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Query chaining — OkNone, 9 no-op maps', () => {
|
||||||
|
const nm = okNone();
|
||||||
|
const fp = E.right(O.none) as E.Either<string, O.Option<number>>;
|
||||||
|
const fn = (x: number) => x + 1;
|
||||||
|
|
||||||
|
bench('native-monad', () => {
|
||||||
|
nm.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts (nested)', () => {
|
||||||
|
pipe(
|
||||||
|
fp,
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
E.map(O.map(fn)),
|
||||||
|
);
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 20-step deep map chain — OkSome path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Query chaining — 20 maps on OkSome', () => {
|
||||||
|
const fn = (x: number) => x + 1;
|
||||||
|
|
||||||
|
bench('native-monad', () => {
|
||||||
|
let r = okSome(0);
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts (nested)', () => {
|
||||||
|
let r = E.right(O.some(0)) as E.Either<string, O.Option<number>>;
|
||||||
|
for (let i = 0; i < 20; i++) r = pipe(r, E.map(O.map(fn)));
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
144
benchmarks/query/collection.bench.ts
Normal file
144
benchmarks/query/collection.bench.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD, EXTENDED } from '../helpers/bench-options.js';
|
||||||
|
import { SIZES, errorIndices } from '../helpers/data-generators.js';
|
||||||
|
import {
|
||||||
|
okSome,
|
||||||
|
okNone,
|
||||||
|
errNone,
|
||||||
|
Query,
|
||||||
|
E,
|
||||||
|
O,
|
||||||
|
pipe,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
import * as RA from 'fp-ts/ReadonlyArray';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Query.all — all OkSome
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of SIZES) {
|
||||||
|
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||||
|
|
||||||
|
const nmAll = Array.from({ length: size }, (_, i) => okSome(i));
|
||||||
|
const fpAll = Array.from({ length: size }, (_, i) =>
|
||||||
|
E.right(O.some(i)) as E.Either<string, O.Option<number>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
describe(`Query.all — ${size} items, all OkSome`, () => {
|
||||||
|
bench('native-monad', () => { Query.all(nmAll); }, opts);
|
||||||
|
bench('fp-ts (nested, manual)', () => {
|
||||||
|
const values: number[] = [];
|
||||||
|
for (const item of fpAll) {
|
||||||
|
if (E.isLeft(item)) return item;
|
||||||
|
if (O.isNone(item.right)) continue;
|
||||||
|
values.push(item.right.value);
|
||||||
|
}
|
||||||
|
return E.right(O.some(values));
|
||||||
|
}, opts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Query.all — mixed (OkSome + OkNone + ErrNone)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of SIZES) {
|
||||||
|
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||||
|
const errIdx = errorIndices(size, 0.1);
|
||||||
|
const noneIdx = errorIndices(size, 0.15);
|
||||||
|
|
||||||
|
const nmMixed = Array.from({ length: size }, (_, i) => {
|
||||||
|
if (errIdx.has(i)) return errNone('fail');
|
||||||
|
if (noneIdx.has(i)) return okNone();
|
||||||
|
return okSome(i);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fpMixed = Array.from({ length: size }, (_, i) => {
|
||||||
|
if (errIdx.has(i)) return E.left('fail') as E.Either<string, O.Option<number>>;
|
||||||
|
if (noneIdx.has(i)) return E.right(O.none) as E.Either<string, O.Option<number>>;
|
||||||
|
return E.right(O.some(i)) as E.Either<string, O.Option<number>>;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`Query.all — ${size} items, mixed (short-circuit)`, () => {
|
||||||
|
bench('native-monad', () => { Query.all(nmMixed); }, opts);
|
||||||
|
bench('fp-ts (nested, manual)', () => {
|
||||||
|
const values: number[] = [];
|
||||||
|
for (const item of fpMixed) {
|
||||||
|
if (E.isLeft(item)) return item;
|
||||||
|
if (O.isNone(item.right)) continue;
|
||||||
|
values.push(item.right.value);
|
||||||
|
}
|
||||||
|
return E.right(O.some(values));
|
||||||
|
}, opts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Query.partition
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of SIZES) {
|
||||||
|
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||||
|
const errIdx = errorIndices(size, 0.1);
|
||||||
|
const noneIdx = errorIndices(size, 0.15);
|
||||||
|
|
||||||
|
const nmMixed = Array.from({ length: size }, (_, i) => {
|
||||||
|
if (errIdx.has(i)) return errNone('fail');
|
||||||
|
if (noneIdx.has(i)) return okNone();
|
||||||
|
return okSome(i);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fpMixed = Array.from({ length: size }, (_, i) => {
|
||||||
|
if (errIdx.has(i)) return E.left('fail') as E.Either<string, O.Option<number>>;
|
||||||
|
if (noneIdx.has(i)) return E.right(O.none) as E.Either<string, O.Option<number>>;
|
||||||
|
return E.right(O.some(i)) as E.Either<string, O.Option<number>>;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`Query.partition — ${size} items`, () => {
|
||||||
|
bench('native-monad', () => { Query.partition(nmMixed); }, opts);
|
||||||
|
bench('fp-ts (nested, manual)', () => {
|
||||||
|
const values: number[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
let noneCount = 0;
|
||||||
|
for (const item of fpMixed) {
|
||||||
|
if (E.isLeft(item)) { errors.push(item.left); continue; }
|
||||||
|
if (O.isNone(item.right)) { noneCount++; continue; }
|
||||||
|
values.push(item.right.value);
|
||||||
|
}
|
||||||
|
return { values, noneCount, errors };
|
||||||
|
}, opts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Query.compact
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of SIZES) {
|
||||||
|
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||||
|
const errIdx = errorIndices(size, 0.1);
|
||||||
|
const noneIdx = errorIndices(size, 0.15);
|
||||||
|
|
||||||
|
const nmMixed = Array.from({ length: size }, (_, i) => {
|
||||||
|
if (errIdx.has(i)) return errNone('fail');
|
||||||
|
if (noneIdx.has(i)) return okNone();
|
||||||
|
return okSome(i);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fpMixed = Array.from({ length: size }, (_, i) => {
|
||||||
|
if (errIdx.has(i)) return E.left('fail') as E.Either<string, O.Option<number>>;
|
||||||
|
if (noneIdx.has(i)) return E.right(O.none) as E.Either<string, O.Option<number>>;
|
||||||
|
return E.right(O.some(i)) as E.Either<string, O.Option<number>>;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`Query.compact — ${size} items`, () => {
|
||||||
|
bench('native-monad', () => { Query.compact(nmMixed); }, opts);
|
||||||
|
bench('fp-ts (nested, manual)', () => {
|
||||||
|
const values: number[] = [];
|
||||||
|
for (const item of fpMixed) {
|
||||||
|
if (E.isRight(item) && O.isSome(item.right)) values.push(item.right.value);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}, opts);
|
||||||
|
});
|
||||||
|
}
|
||||||
65
benchmarks/query/construction.bench.ts
Normal file
65
benchmarks/query/construction.bench.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
okSome,
|
||||||
|
okNone,
|
||||||
|
errNone,
|
||||||
|
TsOk,
|
||||||
|
TsErr,
|
||||||
|
TsSome,
|
||||||
|
TsNone,
|
||||||
|
OxOk,
|
||||||
|
OxErr,
|
||||||
|
OxSome,
|
||||||
|
OxNone,
|
||||||
|
E,
|
||||||
|
O,
|
||||||
|
TmResult,
|
||||||
|
TmMaybe,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// OkSome — vs nested Result<Option<T>, E>
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Query construction — OkSome (primitive)', () => {
|
||||||
|
bench('native-monad', () => { okSome(42); }, STANDARD);
|
||||||
|
bench('ts-results-es (nested)', () => { TsOk(TsSome(42)); }, STANDARD);
|
||||||
|
bench('oxide.ts (nested)', () => { OxOk(OxSome(42)); }, STANDARD);
|
||||||
|
bench('fp-ts (nested)', () => { E.right(O.some(42)); }, STANDARD);
|
||||||
|
bench('true-myth (nested)', () => { TmResult.ok(TmMaybe.just(42)); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Query construction — OkSome (object payload)', () => {
|
||||||
|
const value = { id: 1, name: 'Alice' };
|
||||||
|
|
||||||
|
bench('native-monad', () => { okSome(value); }, STANDARD);
|
||||||
|
bench('ts-results-es (nested)', () => { TsOk(TsSome(value)); }, STANDARD);
|
||||||
|
bench('oxide.ts (nested)', () => { OxOk(OxSome(value)); }, STANDARD);
|
||||||
|
bench('fp-ts (nested)', () => { E.right(O.some(value)); }, STANDARD);
|
||||||
|
bench('true-myth (nested)', () => { TmResult.ok(TmMaybe.just(value)); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// OkNone — vs nested Result<Option<T>, E>
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Query construction — OkNone (singleton)', () => {
|
||||||
|
bench('native-monad', () => { okNone(); }, STANDARD);
|
||||||
|
bench('ts-results-es (nested)', () => { TsOk(TsNone); }, STANDARD);
|
||||||
|
bench('oxide.ts (nested)', () => { OxOk(OxNone); }, STANDARD);
|
||||||
|
bench('fp-ts (nested)', () => { E.right(O.none); }, STANDARD);
|
||||||
|
bench('true-myth (nested)', () => { TmResult.ok(TmMaybe.nothing()); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ErrNone — vs nested Err
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Query construction — ErrNone', () => {
|
||||||
|
bench('native-monad', () => { errNone('fail'); }, STANDARD);
|
||||||
|
bench('ts-results-es (nested)', () => { TsErr('fail'); }, STANDARD);
|
||||||
|
bench('oxide.ts (nested)', () => { OxErr('fail'); }, STANDARD);
|
||||||
|
bench('fp-ts (nested)', () => { E.left('fail'); }, STANDARD);
|
||||||
|
bench('true-myth (nested)', () => { TmResult.err('fail'); }, STANDARD);
|
||||||
|
});
|
||||||
172
benchmarks/query/instance-methods.bench.ts
Normal file
172
benchmarks/query/instance-methods.bench.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
okSome,
|
||||||
|
okNone,
|
||||||
|
errNone,
|
||||||
|
E,
|
||||||
|
O,
|
||||||
|
pipe,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
const double = (x: number) => x * 2;
|
||||||
|
const toUpper = (e: string) => e.toUpperCase();
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// map — OkSome path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Query.map — OkSome path', () => {
|
||||||
|
const nm = okSome(42);
|
||||||
|
const fp = E.right(O.some(42));
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.map(double); }, STANDARD);
|
||||||
|
bench('fp-ts (nested)', () => {
|
||||||
|
pipe(fp, E.map(O.map(double)));
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// map — OkNone path (no-op)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Query.map — OkNone path (no-op)', () => {
|
||||||
|
const nm = okNone();
|
||||||
|
const fp = E.right(O.none);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.map(double); }, STANDARD);
|
||||||
|
bench('fp-ts (nested)', () => {
|
||||||
|
pipe(fp, E.map(O.map(double)));
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// map — ErrNone path (no-op)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Query.map — ErrNone path (no-op)', () => {
|
||||||
|
const nm = errNone('fail');
|
||||||
|
const fp = E.left('fail');
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.map(double); }, STANDARD);
|
||||||
|
bench('fp-ts (nested)', () => {
|
||||||
|
pipe(fp, E.map(O.map(double)));
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// flatMap — OkSome path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Query.flatMap — OkSome path', () => {
|
||||||
|
const nm = okSome(42);
|
||||||
|
const fp = E.right(O.some(42));
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.flatMap(x => okSome(x * 2)); }, STANDARD);
|
||||||
|
bench('fp-ts (nested)', () => {
|
||||||
|
pipe(
|
||||||
|
fp,
|
||||||
|
E.flatMap(inner =>
|
||||||
|
pipe(
|
||||||
|
inner,
|
||||||
|
O.match(
|
||||||
|
() => E.right(O.none) as E.Either<string, O.Option<number>>,
|
||||||
|
v => E.right(O.some(v * 2)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// mapErr — ErrNone path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Query.mapErr — ErrNone path', () => {
|
||||||
|
const nm = errNone('fail');
|
||||||
|
const fp = E.left('fail');
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.mapErr(toUpper); }, STANDARD);
|
||||||
|
bench('fp-ts (nested)', () => {
|
||||||
|
pipe(fp, E.mapLeft(toUpper));
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// match — three-way
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Query.match — OkSome (three-way dispatch)', () => {
|
||||||
|
const nm = okSome(42);
|
||||||
|
const fp = E.right(O.some(42));
|
||||||
|
|
||||||
|
bench('native-monad', () => {
|
||||||
|
nm.match(v => v * 2, () => 0, () => -1);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts (nested, two match calls)', () => {
|
||||||
|
pipe(
|
||||||
|
fp,
|
||||||
|
E.match(
|
||||||
|
() => -1,
|
||||||
|
O.match(() => 0, v => v * 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Query.match — OkNone (three-way dispatch)', () => {
|
||||||
|
const nm = okNone();
|
||||||
|
const fp = E.right(O.none);
|
||||||
|
|
||||||
|
bench('native-monad', () => {
|
||||||
|
nm.match(() => 1, () => 0, () => -1);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts (nested)', () => {
|
||||||
|
pipe(
|
||||||
|
fp,
|
||||||
|
E.match(
|
||||||
|
() => -1,
|
||||||
|
O.match(() => 0, () => 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Query.match — ErrNone (three-way dispatch)', () => {
|
||||||
|
const nm = errNone('fail');
|
||||||
|
const fp = E.left('fail');
|
||||||
|
|
||||||
|
bench('native-monad', () => {
|
||||||
|
nm.match(() => 1, () => 0, e => e.length);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts (nested)', () => {
|
||||||
|
pipe(
|
||||||
|
fp,
|
||||||
|
E.match(
|
||||||
|
e => e.length,
|
||||||
|
O.match(() => 0, () => 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// filter — OkSome path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Query.filter — predicate passes', () => {
|
||||||
|
const nm = okSome(42);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.filter(x => x > 0); }, STANDARD);
|
||||||
|
// No other library has a three-state filter
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Query.filter — predicate fails', () => {
|
||||||
|
const nm = okSome(-1);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.filter(x => x > 0); }, STANDARD);
|
||||||
|
});
|
||||||
70
benchmarks/result/async.bench.ts
Normal file
70
benchmarks/result/async.bench.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { EXTENDED } from '../helpers/bench-options.js';
|
||||||
|
import { SIZES } from '../helpers/data-generators.js';
|
||||||
|
import {
|
||||||
|
ok,
|
||||||
|
err,
|
||||||
|
Result,
|
||||||
|
ResultPromise,
|
||||||
|
ntOk,
|
||||||
|
ntErr,
|
||||||
|
NtResultAsync,
|
||||||
|
E,
|
||||||
|
pipe,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
import * as TE from 'fp-ts/TaskEither';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ResultPromise.from — success
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('ResultPromise.from — success', () => {
|
||||||
|
bench('native-monad', async () => {
|
||||||
|
await ResultPromise.from(() => Promise.resolve(42));
|
||||||
|
}, EXTENDED);
|
||||||
|
|
||||||
|
bench('neverthrow', async () => {
|
||||||
|
await NtResultAsync.fromPromise(Promise.resolve(42), () => 'err');
|
||||||
|
}, EXTENDED);
|
||||||
|
|
||||||
|
bench('fp-ts', async () => {
|
||||||
|
await TE.tryCatch(() => Promise.resolve(42), () => 'err')();
|
||||||
|
}, EXTENDED);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ResultPromise.from — failure
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('ResultPromise.from — failure', () => {
|
||||||
|
bench('native-monad', async () => {
|
||||||
|
await ResultPromise.from(() => Promise.reject(new Error('fail')), () => 'err');
|
||||||
|
}, EXTENDED);
|
||||||
|
|
||||||
|
bench('neverthrow', async () => {
|
||||||
|
await NtResultAsync.fromPromise(Promise.reject(new Error('fail')), () => 'err');
|
||||||
|
}, EXTENDED);
|
||||||
|
|
||||||
|
bench('fp-ts', async () => {
|
||||||
|
await TE.tryCatch(() => Promise.reject(new Error('fail')), () => 'err')();
|
||||||
|
}, EXTENDED);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ResultPromise.all — all Ok
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of [10, 100] as const) {
|
||||||
|
describe(`ResultPromise.all — ${size} items, all Ok`, () => {
|
||||||
|
const nmPromises = () => Array.from({ length: size }, (_, i) => Promise.resolve(ok(i)));
|
||||||
|
const ntPromises = () => Array.from({ length: size }, (_, i) => NtResultAsync.fromSafePromise(Promise.resolve(i)));
|
||||||
|
|
||||||
|
bench('native-monad', async () => {
|
||||||
|
await ResultPromise.all(nmPromises());
|
||||||
|
}, EXTENDED);
|
||||||
|
|
||||||
|
bench('neverthrow', async () => {
|
||||||
|
await NtResultAsync.combine(ntPromises());
|
||||||
|
}, EXTENDED);
|
||||||
|
});
|
||||||
|
}
|
||||||
153
benchmarks/result/chaining.bench.ts
Normal file
153
benchmarks/result/chaining.bench.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
ok,
|
||||||
|
err,
|
||||||
|
ntOk,
|
||||||
|
ntErr,
|
||||||
|
TsOk,
|
||||||
|
TsErr,
|
||||||
|
OxOk,
|
||||||
|
OxErr,
|
||||||
|
E,
|
||||||
|
pipe,
|
||||||
|
TmResult,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 5-step pipeline — success path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result chaining — 5-step pipeline (all succeed)', () => {
|
||||||
|
bench('native-monad', () => {
|
||||||
|
ok(42)
|
||||||
|
.map(x => x * 2)
|
||||||
|
.flatMap(x => (x > 0 ? ok(x) : err('negative')))
|
||||||
|
.map(x => ({ value: x }))
|
||||||
|
.flatMap(x => ok({ ...x, label: 'result' }))
|
||||||
|
.map(x => JSON.stringify(x));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('neverthrow', () => {
|
||||||
|
ntOk(42)
|
||||||
|
.map(x => x * 2)
|
||||||
|
.andThen(x => (x > 0 ? ntOk(x) : ntErr('negative')))
|
||||||
|
.map(x => ({ value: x }))
|
||||||
|
.andThen(x => ntOk({ ...x, label: 'result' }))
|
||||||
|
.map(x => JSON.stringify(x));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('ts-results-es', () => {
|
||||||
|
TsOk<number, string>(42)
|
||||||
|
.map(x => x * 2)
|
||||||
|
.andThen(x => (x > 0 ? TsOk(x) : TsErr('negative')))
|
||||||
|
.map(x => ({ value: x }))
|
||||||
|
.andThen(x => TsOk<{ value: number; label: string }, string>({ ...x, label: 'result' }))
|
||||||
|
.map(x => JSON.stringify(x));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('oxide.ts', () => {
|
||||||
|
OxOk(42)
|
||||||
|
.map(x => x * 2)
|
||||||
|
.andThen(x => (x > 0 ? OxOk(x) : OxErr('negative')))
|
||||||
|
.map(x => ({ value: x }))
|
||||||
|
.andThen(x => OxOk({ ...x, label: 'result' }))
|
||||||
|
.map(x => JSON.stringify(x));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts', () => {
|
||||||
|
pipe(
|
||||||
|
E.right(42),
|
||||||
|
E.map(x => x * 2),
|
||||||
|
E.flatMap(x => (x > 0 ? E.right(x) : E.left('negative'))),
|
||||||
|
E.map(x => ({ value: x })),
|
||||||
|
E.flatMap(x => E.right({ ...x, label: 'result' })),
|
||||||
|
E.map(x => JSON.stringify(x)),
|
||||||
|
);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('true-myth', () => {
|
||||||
|
TmResult.ok<number, string>(42)
|
||||||
|
.map(x => x * 2)
|
||||||
|
.andThen(x => (x > 0 ? TmResult.ok(x) : TmResult.err('negative')))
|
||||||
|
.map(x => ({ value: x }))
|
||||||
|
.andThen(x => TmResult.ok({ ...x, label: 'result' }))
|
||||||
|
.map(x => JSON.stringify(x));
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 10-step pipeline — error at step 1 (9 no-ops)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result chaining — error at step 1, 9 no-op maps', () => {
|
||||||
|
const nmErr = err('fail');
|
||||||
|
const ntErrVal = ntErr('fail');
|
||||||
|
const tsErr = TsErr<number, string>('fail');
|
||||||
|
const oxErr = OxErr('fail');
|
||||||
|
const fpErr = E.left('fail');
|
||||||
|
const tmErr = TmResult.err<number, string>('fail');
|
||||||
|
const fn = (x: number) => x + 1;
|
||||||
|
|
||||||
|
bench('native-monad', () => {
|
||||||
|
nmErr.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('neverthrow', () => {
|
||||||
|
ntErrVal.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('ts-results-es', () => {
|
||||||
|
tsErr.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('oxide.ts', () => {
|
||||||
|
oxErr.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts', () => {
|
||||||
|
pipe(fpErr, E.map(fn), E.map(fn), E.map(fn), E.map(fn), E.map(fn), E.map(fn), E.map(fn), E.map(fn), E.map(fn));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('true-myth', () => {
|
||||||
|
tmErr.map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn).map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 20-step deep map chain — success path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result chaining — 20 maps on Ok', () => {
|
||||||
|
const fn = (x: number) => x + 1;
|
||||||
|
|
||||||
|
bench('native-monad', () => {
|
||||||
|
let r = ok(0);
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('neverthrow', () => {
|
||||||
|
let r = ntOk(0);
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('ts-results-es', () => {
|
||||||
|
let r = TsOk<number, string>(0);
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn) as any;
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('oxide.ts', () => {
|
||||||
|
let r = OxOk(0);
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn) as any;
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts', () => {
|
||||||
|
let r = E.right(0) as E.Either<string, number>;
|
||||||
|
for (let i = 0; i < 20; i++) r = pipe(r, E.map(fn));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('true-myth', () => {
|
||||||
|
let r = TmResult.ok<number, string>(0);
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn) as any;
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
177
benchmarks/result/collection.bench.ts
Normal file
177
benchmarks/result/collection.bench.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD, EXTENDED } from '../helpers/bench-options.js';
|
||||||
|
import { SIZES, errorIndices } from '../helpers/data-generators.js';
|
||||||
|
import {
|
||||||
|
ok,
|
||||||
|
err,
|
||||||
|
Result,
|
||||||
|
ntOk,
|
||||||
|
ntErr,
|
||||||
|
NtResult,
|
||||||
|
TsOk,
|
||||||
|
TsErr,
|
||||||
|
TsResult,
|
||||||
|
OxOk,
|
||||||
|
OxErr,
|
||||||
|
E,
|
||||||
|
pipe,
|
||||||
|
TmResult,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
import * as RA from 'fp-ts/ReadonlyArray';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Result.all — all Ok
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of SIZES) {
|
||||||
|
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||||
|
|
||||||
|
const nmAll = Array.from({ length: size }, (_, i) => ok(i));
|
||||||
|
const ntAll = Array.from({ length: size }, (_, i) => ntOk(i));
|
||||||
|
const tsAll = Array.from({ length: size }, (_, i) => TsOk<number, string>(i));
|
||||||
|
const oxAll = Array.from({ length: size }, (_, i) => OxOk(i));
|
||||||
|
const fpAll = Array.from({ length: size }, (_, i) => E.right(i));
|
||||||
|
const tmAll = Array.from({ length: size }, (_, i) => TmResult.ok<number, string>(i));
|
||||||
|
|
||||||
|
describe(`Result.all — ${size} items, all Ok`, () => {
|
||||||
|
bench('native-monad', () => { Result.all(nmAll); }, opts);
|
||||||
|
bench('neverthrow', () => { NtResult.combine(ntAll); }, opts);
|
||||||
|
bench('ts-results-es', () => { TsResult.all(...tsAll); }, opts);
|
||||||
|
// oxide.ts: no built-in all/combine
|
||||||
|
bench('fp-ts', () => { pipe(fpAll, RA.sequence(E.Applicative)); }, opts);
|
||||||
|
// true-myth: no built-in all
|
||||||
|
bench('true-myth (manual)', () => {
|
||||||
|
const values: number[] = [];
|
||||||
|
for (const r of tmAll) {
|
||||||
|
if (TmResult.isErr(r)) return r;
|
||||||
|
values.push((r as { value: number }).value);
|
||||||
|
}
|
||||||
|
return TmResult.ok(values);
|
||||||
|
}, opts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Result.all — 20% Err (short-circuit)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of SIZES) {
|
||||||
|
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||||
|
const errIdx = errorIndices(size, 0.2);
|
||||||
|
|
||||||
|
const nmMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
errIdx.has(i) ? err('fail') : ok(i),
|
||||||
|
);
|
||||||
|
const ntMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
errIdx.has(i) ? ntErr('fail') : ntOk(i),
|
||||||
|
);
|
||||||
|
const tsMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
errIdx.has(i) ? TsErr<number, string>('fail') : TsOk<number, string>(i),
|
||||||
|
);
|
||||||
|
const fpMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
errIdx.has(i) ? E.left('fail' as const) : E.right(i),
|
||||||
|
);
|
||||||
|
const tmMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
errIdx.has(i) ? TmResult.err<number, string>('fail') : TmResult.ok<number, string>(i),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe(`Result.all — ${size} items, 20% Err (short-circuit)`, () => {
|
||||||
|
bench('native-monad', () => { Result.all(nmMixed); }, opts);
|
||||||
|
bench('neverthrow', () => { NtResult.combine(ntMixed as any); }, opts);
|
||||||
|
bench('ts-results-es', () => { TsResult.all(...tsMixed); }, opts);
|
||||||
|
bench('fp-ts', () => { pipe(fpMixed, RA.sequence(E.Applicative)); }, opts);
|
||||||
|
bench('true-myth (manual)', () => {
|
||||||
|
const values: number[] = [];
|
||||||
|
for (const r of tmMixed) {
|
||||||
|
if (TmResult.isErr(r)) return r;
|
||||||
|
values.push((r as { value: number }).value);
|
||||||
|
}
|
||||||
|
return TmResult.ok(values);
|
||||||
|
}, opts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Result.partition
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of SIZES) {
|
||||||
|
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||||
|
const errIdx = errorIndices(size, 0.2);
|
||||||
|
|
||||||
|
const nmMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
errIdx.has(i) ? err('fail') : ok(i),
|
||||||
|
);
|
||||||
|
const ntMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
errIdx.has(i) ? ntErr('fail') : ntOk(i),
|
||||||
|
);
|
||||||
|
const fpMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
errIdx.has(i) ? E.left('fail' as const) : E.right(i),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe(`Result.partition — ${size} items`, () => {
|
||||||
|
bench('native-monad', () => { Result.partition(nmMixed); }, opts);
|
||||||
|
bench('neverthrow (manual)', () => {
|
||||||
|
const values: number[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
for (const r of ntMixed) {
|
||||||
|
if (r.isOk()) values.push(r.value);
|
||||||
|
else errors.push(r.error);
|
||||||
|
}
|
||||||
|
return { values, errors };
|
||||||
|
}, opts);
|
||||||
|
bench('fp-ts', () => { RA.separate(fpMixed); }, opts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Result.compact
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of SIZES) {
|
||||||
|
const opts = size >= 10_000 ? EXTENDED : STANDARD;
|
||||||
|
const errIdx = errorIndices(size, 0.2);
|
||||||
|
|
||||||
|
const nmMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
errIdx.has(i) ? err('fail') : ok(i),
|
||||||
|
);
|
||||||
|
const ntMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
errIdx.has(i) ? ntErr('fail') : ntOk(i),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe(`Result.compact — ${size} items`, () => {
|
||||||
|
bench('native-monad', () => { Result.compact(nmMixed); }, opts);
|
||||||
|
bench('neverthrow (manual)', () => {
|
||||||
|
const values: number[] = [];
|
||||||
|
for (const r of ntMixed) {
|
||||||
|
if (r.isOk()) values.push(r.value);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}, opts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Result.some / Result.every
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for (const size of [100, 1_000]) {
|
||||||
|
const errIdx = errorIndices(size, 0.2);
|
||||||
|
|
||||||
|
const nmMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
errIdx.has(i) ? err('fail') : ok(i),
|
||||||
|
);
|
||||||
|
const ntMixed = Array.from({ length: size }, (_, i) =>
|
||||||
|
errIdx.has(i) ? ntErr('fail') : ntOk(i),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe(`Result.some — ${size} items`, () => {
|
||||||
|
bench('native-monad', () => { Result.some(nmMixed); }, STANDARD);
|
||||||
|
bench('neverthrow (manual)', () => { ntMixed.some(r => r.isOk()); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`Result.every — ${size} items`, () => {
|
||||||
|
bench('native-monad', () => { Result.every(nmMixed); }, STANDARD);
|
||||||
|
bench('neverthrow (manual)', () => { ntMixed.every(r => r.isOk()); }, STANDARD);
|
||||||
|
});
|
||||||
|
}
|
||||||
59
benchmarks/result/construction.bench.ts
Normal file
59
benchmarks/result/construction.bench.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
ok,
|
||||||
|
err,
|
||||||
|
ntOk,
|
||||||
|
ntErr,
|
||||||
|
TsOk,
|
||||||
|
TsErr,
|
||||||
|
OxOk,
|
||||||
|
OxErr,
|
||||||
|
E,
|
||||||
|
pipe,
|
||||||
|
TmResult,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Ok / Right / success construction
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result construction — Ok (primitive)', () => {
|
||||||
|
bench('native-monad', () => { ok(42); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { ntOk(42); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { TsOk(42); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { OxOk(42); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { E.right(42); }, STANDARD);
|
||||||
|
bench('true-myth', () => { TmResult.ok(42); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Result construction — Err (primitive)', () => {
|
||||||
|
bench('native-monad', () => { err('fail'); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { ntErr('fail'); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { TsErr('fail'); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { OxErr('fail'); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { E.left('fail'); }, STANDARD);
|
||||||
|
bench('true-myth', () => { TmResult.err('fail'); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Result construction — Ok (object payload)', () => {
|
||||||
|
const value = { id: 1, name: 'Alice', roles: ['admin', 'user'] };
|
||||||
|
|
||||||
|
bench('native-monad', () => { ok(value); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { ntOk(value); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { TsOk(value); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { OxOk(value); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { E.right(value); }, STANDARD);
|
||||||
|
bench('true-myth', () => { TmResult.ok(value); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Result construction — Err (Error object)', () => {
|
||||||
|
const error = new Error('something went wrong');
|
||||||
|
|
||||||
|
bench('native-monad', () => { err(error); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { ntErr(error); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { TsErr(error); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { OxErr(error); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { E.left(error); }, STANDARD);
|
||||||
|
bench('true-myth', () => { TmResult.err(error); }, STANDARD);
|
||||||
|
});
|
||||||
68
benchmarks/result/conversion.bench.ts
Normal file
68
benchmarks/result/conversion.bench.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
ok,
|
||||||
|
err,
|
||||||
|
ntOk,
|
||||||
|
ntErr,
|
||||||
|
TsOk,
|
||||||
|
TsErr,
|
||||||
|
OxOk,
|
||||||
|
OxErr,
|
||||||
|
E,
|
||||||
|
pipe,
|
||||||
|
TmResult,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// toNullable — success path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.toNullable — Ok', () => {
|
||||||
|
const nm = ok(42);
|
||||||
|
const ox = OxOk(42);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.toNullable(); }, STANDARD);
|
||||||
|
// neverthrow: no toNullable — use match or _unsafeUnwrap
|
||||||
|
// ts-results-es: no toNullable
|
||||||
|
bench('oxide.ts', () => { ox.into(); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(E.right(42), E.toUnion); }, STANDARD);
|
||||||
|
bench('true-myth', () => { TmResult.ok<number, string>(42).isOk ? 42 : null; }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// toNullable — failure path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.toNullable — Err', () => {
|
||||||
|
const nm = err('fail');
|
||||||
|
const ox = OxErr('fail');
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.toNullable(); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.into(); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(E.left('fail'), E.toUnion); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// toString
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.toString — Ok', () => {
|
||||||
|
const nm = ok(42);
|
||||||
|
const ox = OxOk(42);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.toString(); }, STANDARD);
|
||||||
|
// neverthrow: no toString
|
||||||
|
// ts-results-es: no toString
|
||||||
|
bench('oxide.ts', () => { ox.toString(); }, STANDARD);
|
||||||
|
// fp-ts: no toString
|
||||||
|
// true-myth: no toString
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Result.toString — Err', () => {
|
||||||
|
const nm = err('fail');
|
||||||
|
const ox = OxErr('fail');
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.toString(); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.toString(); }, STANDARD);
|
||||||
|
});
|
||||||
39
benchmarks/result/from-wrapping.bench.ts
Normal file
39
benchmarks/result/from-wrapping.bench.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
Result,
|
||||||
|
ntFromThrowable,
|
||||||
|
E,
|
||||||
|
TmResult,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Result.from — success (no throw)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.from — success (no throw)', () => {
|
||||||
|
const fn = () => 42;
|
||||||
|
|
||||||
|
bench('native-monad', () => { Result.from(fn); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { ntFromThrowable(fn)(); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { E.tryCatch(fn, () => 'error'); }, STANDARD);
|
||||||
|
bench('true-myth', () => { TmResult.tryOr('error', fn); }, STANDARD);
|
||||||
|
// ts-results-es: no from/tryCatch
|
||||||
|
// oxide.ts: no from/tryCatch
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Result.from — failure (throws)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.from — failure (throws)', () => {
|
||||||
|
const thrower = () => {
|
||||||
|
throw new Error('fail');
|
||||||
|
};
|
||||||
|
const mapErr = () => 'error' as const;
|
||||||
|
|
||||||
|
bench('native-monad', () => { Result.from(thrower, mapErr); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { ntFromThrowable(thrower, mapErr)(); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { E.tryCatch(thrower, mapErr); }, STANDARD);
|
||||||
|
bench('true-myth', () => { TmResult.tryOr('error', thrower); }, STANDARD);
|
||||||
|
});
|
||||||
208
benchmarks/result/instance-methods.bench.ts
Normal file
208
benchmarks/result/instance-methods.bench.ts
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
ok,
|
||||||
|
err,
|
||||||
|
ntOk,
|
||||||
|
ntErr,
|
||||||
|
TsOk,
|
||||||
|
TsErr,
|
||||||
|
OxOk,
|
||||||
|
OxErr,
|
||||||
|
oxMatch,
|
||||||
|
E,
|
||||||
|
pipe,
|
||||||
|
TmResult,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
const double = (x: number) => x * 2;
|
||||||
|
const toUpper = (e: string) => e.toUpperCase();
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// map — success path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.map — success path', () => {
|
||||||
|
const nm = ok(42);
|
||||||
|
const nt = ntOk(42);
|
||||||
|
const ts = TsOk(42);
|
||||||
|
const ox = OxOk(42);
|
||||||
|
const fp = E.right(42);
|
||||||
|
const tm = TmResult.ok<number, string>(42);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.map(double); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { nt.map(double); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { ts.map(double); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.map(double); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, E.map(double)); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.map(double); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// map — failure path (no-op)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.map — failure path (no-op)', () => {
|
||||||
|
const nm = err('fail');
|
||||||
|
const nt = ntErr('fail');
|
||||||
|
const ts = TsErr('fail');
|
||||||
|
const ox = OxErr('fail');
|
||||||
|
const fp = E.left('fail');
|
||||||
|
const tm = TmResult.err<number, string>('fail');
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.map(double); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { nt.map(double); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { ts.map(double); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.map(double); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, E.map(double)); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.map(double); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// flatMap — success path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.flatMap — success path', () => {
|
||||||
|
const nm = ok(42);
|
||||||
|
const nt = ntOk(42);
|
||||||
|
const ts = TsOk<number, string>(42);
|
||||||
|
const ox = OxOk(42);
|
||||||
|
const fp = E.right(42);
|
||||||
|
const tm = TmResult.ok<number, string>(42);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.flatMap(x => ok(x * 2)); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { nt.andThen(x => ntOk(x * 2)); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { ts.andThen(x => TsOk(x * 2)); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.andThen(x => OxOk(x * 2)); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, E.flatMap(x => E.right(x * 2))); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.andThen(x => TmResult.ok(x * 2)); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// flatMap — failure path (no-op)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.flatMap — failure path (no-op)', () => {
|
||||||
|
const nm = err('fail');
|
||||||
|
const nt = ntErr('fail');
|
||||||
|
const ts = TsErr<number, string>('fail');
|
||||||
|
const ox = OxErr('fail');
|
||||||
|
const fp = E.left('fail');
|
||||||
|
const tm = TmResult.err<number, string>('fail');
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.flatMap(x => ok(x * 2)); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { nt.andThen(x => ntOk(x * 2)); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { ts.andThen(x => TsOk(x * 2)); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.andThen(x => OxOk(x * 2)); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, E.flatMap(x => E.right(x * 2))); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.andThen(x => TmResult.ok(x * 2)); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// mapErr — error path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.mapErr — error path', () => {
|
||||||
|
const nm = err('fail');
|
||||||
|
const nt = ntErr('fail');
|
||||||
|
const ts = TsErr<number, string>('fail');
|
||||||
|
const ox = OxErr('fail');
|
||||||
|
const fp = E.left('fail');
|
||||||
|
const tm = TmResult.err<number, string>('fail');
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.mapErr(toUpper); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { nt.mapErr(toUpper); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { ts.mapErr(toUpper); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.mapErr(toUpper); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, E.mapLeft(toUpper)); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.mapErr(toUpper); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// mapErr — success path (no-op)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.mapErr — success path (no-op)', () => {
|
||||||
|
const nm = ok(42);
|
||||||
|
const nt = ntOk(42);
|
||||||
|
const ts = TsOk<number, string>(42);
|
||||||
|
const ox = OxOk(42);
|
||||||
|
const fp = E.right(42);
|
||||||
|
const tm = TmResult.ok<number, string>(42);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.mapErr(toUpper); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { nt.mapErr(toUpper); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { ts.mapErr(toUpper); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.mapErr(toUpper); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, E.mapLeft(toUpper)); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.mapErr(toUpper); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// match — success path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.match — success path', () => {
|
||||||
|
const nm = ok(42);
|
||||||
|
const nt = ntOk(42);
|
||||||
|
const ox = OxOk(42);
|
||||||
|
const fp = E.right(42);
|
||||||
|
const tm = TmResult.ok<number, string>(42);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.match(x => x * 2, () => 0); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { nt.match(x => x * 2, () => 0); }, STANDARD);
|
||||||
|
// ts-results-es has no match — skip
|
||||||
|
bench('oxide.ts', () => { oxMatch(ox, { Ok: (x: number) => x * 2, Err: () => 0 }); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, E.match(() => 0, x => x * 2)); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.match({ Ok: x => x * 2, Err: () => 0 }); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// match — failure path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.match — failure path', () => {
|
||||||
|
const nm = err('fail');
|
||||||
|
const nt = ntErr('fail');
|
||||||
|
const ox = OxErr('fail');
|
||||||
|
const fp = E.left('fail');
|
||||||
|
const tm = TmResult.err<number, string>('fail');
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.match(() => 0, e => e.length); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { nt.match(() => 0, e => e.length); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { oxMatch(ox, { Ok: () => 0, Err: (e: string) => e.length }); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { pipe(fp, E.match(e => e.length, () => 0)); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.match({ Ok: () => 0, Err: e => e.length }); }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// filter — success path (predicate passes)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.filter — predicate passes', () => {
|
||||||
|
const nm = ok(42);
|
||||||
|
const fp = E.right(42);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.filter(x => x > 0, () => 'negative'); }, STANDARD);
|
||||||
|
// neverthrow: no filter — skip
|
||||||
|
// ts-results-es: no filter — skip
|
||||||
|
// oxide.ts: filter returns Option, not Result — different semantics, skip
|
||||||
|
bench('fp-ts', () => {
|
||||||
|
pipe(fp, E.filterOrElse(x => x > 0, () => 'negative' as const));
|
||||||
|
}, STANDARD);
|
||||||
|
// true-myth: no filter — skip
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// filter — predicate fails
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.filter — predicate fails', () => {
|
||||||
|
const nm = ok(-1);
|
||||||
|
const fp = E.right(-1);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.filter(x => x > 0, () => 'negative'); }, STANDARD);
|
||||||
|
bench('fp-ts', () => {
|
||||||
|
pipe(fp, E.filterOrElse(x => x > 0, () => 'negative' as const));
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
74
benchmarks/result/type-guards.bench.ts
Normal file
74
benchmarks/result/type-guards.bench.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
ok,
|
||||||
|
err,
|
||||||
|
ntOk,
|
||||||
|
ntErr,
|
||||||
|
TsOk,
|
||||||
|
TsErr,
|
||||||
|
OxOk,
|
||||||
|
OxErr,
|
||||||
|
E,
|
||||||
|
TmResult,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// isOk — on Ok variant
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.isOk — on Ok', () => {
|
||||||
|
const nm = ok(42);
|
||||||
|
const nt = ntOk(42);
|
||||||
|
const ts = TsOk(42);
|
||||||
|
const ox = OxOk(42);
|
||||||
|
const fp = E.right(42);
|
||||||
|
const tm = TmResult.ok<number, string>(42);
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.isOk(); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { nt.isOk(); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { ts.isOk(); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.isOk(); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { E.isRight(fp); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.isOk; }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// isOk — on Err variant
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.isOk — on Err', () => {
|
||||||
|
const nm = err('fail');
|
||||||
|
const nt = ntErr('fail');
|
||||||
|
const ts = TsErr('fail');
|
||||||
|
const ox = OxErr('fail');
|
||||||
|
const fp = E.left('fail');
|
||||||
|
const tm = TmResult.err<number, string>('fail');
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.isOk(); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { nt.isOk(); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { ts.isOk(); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.isOk(); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { E.isRight(fp); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.isOk; }, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// isErr — on Err variant
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Result.isErr — on Err', () => {
|
||||||
|
const nm = err('fail');
|
||||||
|
const nt = ntErr('fail');
|
||||||
|
const ts = TsErr('fail');
|
||||||
|
const ox = OxErr('fail');
|
||||||
|
const fp = E.left('fail');
|
||||||
|
const tm = TmResult.err<number, string>('fail');
|
||||||
|
|
||||||
|
bench('native-monad', () => { nm.isErr(); }, STANDARD);
|
||||||
|
bench('neverthrow', () => { nt.isErr(); }, STANDARD);
|
||||||
|
bench('ts-results-es', () => { ts.isErr(); }, STANDARD);
|
||||||
|
bench('oxide.ts', () => { ox.isErr(); }, STANDARD);
|
||||||
|
bench('fp-ts', () => { E.isLeft(fp); }, STANDARD);
|
||||||
|
bench('true-myth', () => { tm.isErr; }, STANDARD);
|
||||||
|
});
|
||||||
145
benchmarks/scenarios/error-propagation.bench.ts
Normal file
145
benchmarks/scenarios/error-propagation.bench.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
ok,
|
||||||
|
err,
|
||||||
|
none,
|
||||||
|
ntOk,
|
||||||
|
ntErr,
|
||||||
|
TsOk,
|
||||||
|
TsErr,
|
||||||
|
TsNone,
|
||||||
|
OxOk,
|
||||||
|
OxErr,
|
||||||
|
OxNone,
|
||||||
|
E,
|
||||||
|
O,
|
||||||
|
pipe,
|
||||||
|
TmResult,
|
||||||
|
TmMaybe,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
const fn = (x: number) => x + 1;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Error propagation through 20 map calls
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Scenario: Err through 20 map calls', () => {
|
||||||
|
const nm = err('fail');
|
||||||
|
const nt = ntErr('fail');
|
||||||
|
const ts = TsErr<number, string>('fail');
|
||||||
|
const ox = OxErr('fail');
|
||||||
|
const fp = E.left('fail') as E.Either<string, number>;
|
||||||
|
const tm = TmResult.err<number, string>('fail');
|
||||||
|
|
||||||
|
bench('native-monad', () => {
|
||||||
|
let r = nm as any;
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('neverthrow', () => {
|
||||||
|
let r = nt as any;
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('ts-results-es', () => {
|
||||||
|
let r = ts as any;
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('oxide.ts', () => {
|
||||||
|
let r = ox as any;
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts', () => {
|
||||||
|
let r = fp;
|
||||||
|
for (let i = 0; i < 20; i++) r = pipe(r, E.map(fn));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('true-myth', () => {
|
||||||
|
let r = tm as any;
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Error propagation through 20 flatMap calls
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Scenario: Err through 20 flatMap calls', () => {
|
||||||
|
const nm = err('fail');
|
||||||
|
const nt = ntErr('fail');
|
||||||
|
const ts = TsErr<number, string>('fail');
|
||||||
|
const ox = OxErr('fail');
|
||||||
|
const fp = E.left('fail') as E.Either<string, number>;
|
||||||
|
const tm = TmResult.err<number, string>('fail');
|
||||||
|
|
||||||
|
bench('native-monad', () => {
|
||||||
|
let r = nm as any;
|
||||||
|
for (let i = 0; i < 20; i++) r = r.flatMap((x: number) => ok(x + 1));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('neverthrow', () => {
|
||||||
|
let r = nt as any;
|
||||||
|
for (let i = 0; i < 20; i++) r = r.andThen((x: number) => ntOk(x + 1));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('ts-results-es', () => {
|
||||||
|
let r = ts as any;
|
||||||
|
for (let i = 0; i < 20; i++) r = r.andThen((x: number) => TsOk(x + 1));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('oxide.ts', () => {
|
||||||
|
let r = ox as any;
|
||||||
|
for (let i = 0; i < 20; i++) r = r.andThen((x: number) => OxOk(x + 1));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts', () => {
|
||||||
|
let r = fp;
|
||||||
|
for (let i = 0; i < 20; i++) r = pipe(r, E.flatMap(x => E.right(x + 1)));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('true-myth', () => {
|
||||||
|
let r = tm as any;
|
||||||
|
for (let i = 0; i < 20; i++) r = r.andThen((x: number) => TmResult.ok(x + 1));
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// None propagation through 20 map calls (Option)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Scenario: None through 20 map calls', () => {
|
||||||
|
const nm = none();
|
||||||
|
const ts = TsNone;
|
||||||
|
const ox = OxNone;
|
||||||
|
const fp = O.none as O.Option<number>;
|
||||||
|
const tm = TmMaybe.nothing<number>();
|
||||||
|
|
||||||
|
bench('native-monad', () => {
|
||||||
|
let r = nm as any;
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('ts-results-es', () => {
|
||||||
|
let r = ts as any;
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('oxide.ts', () => {
|
||||||
|
let r = ox as any;
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts', () => {
|
||||||
|
let r = fp;
|
||||||
|
for (let i = 0; i < 20; i++) r = pipe(r, O.map(fn));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('true-myth', () => {
|
||||||
|
let r = tm as any;
|
||||||
|
for (let i = 0; i < 20; i++) r = r.map(fn);
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
171
benchmarks/scenarios/memory.bench.ts
Normal file
171
benchmarks/scenarios/memory.bench.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { QUICK } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
ok,
|
||||||
|
err,
|
||||||
|
some,
|
||||||
|
none,
|
||||||
|
okSome,
|
||||||
|
okNone,
|
||||||
|
errNone,
|
||||||
|
ntOk,
|
||||||
|
ntErr,
|
||||||
|
TsOk,
|
||||||
|
TsErr,
|
||||||
|
TsSome,
|
||||||
|
TsNone,
|
||||||
|
OxOk,
|
||||||
|
OxErr,
|
||||||
|
OxSome,
|
||||||
|
OxNone,
|
||||||
|
E,
|
||||||
|
O,
|
||||||
|
TmResult,
|
||||||
|
TmMaybe,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Singleton reuse: None / OkNone
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Memory: 1000x None creation (singleton vs constructor)', () => {
|
||||||
|
bench('native-monad none() — frozen singleton', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) none();
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('ts-results-es None — constant', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) { const _ = TsNone; }
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('oxide.ts None — constant', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) { const _ = OxNone; }
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('fp-ts O.none — constant', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) { const _ = O.none; }
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('true-myth Maybe.nothing() — constructor', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) TmMaybe.nothing();
|
||||||
|
}, QUICK);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Memory: 1000x OkNone creation', () => {
|
||||||
|
bench('native-monad okNone() — frozen singleton', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) okNone();
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('fp-ts E.right(O.none) — two allocations', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) E.right(O.none);
|
||||||
|
}, QUICK);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Constructor allocation: Ok / Some / OkSome
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Memory: 1000x Ok construction', () => {
|
||||||
|
bench('native-monad ok(i) — class instance', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) ok(i);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('neverthrow ok(i) — class instance', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) ntOk(i);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('ts-results-es Ok(i) — class instance', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) TsOk(i);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('oxide.ts Ok(i) — tagged object', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) OxOk(i);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('fp-ts E.right(i) — tagged object', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) E.right(i);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('true-myth Result.ok(i) — class instance', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) TmResult.ok(i);
|
||||||
|
}, QUICK);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Memory: 1000x Some construction', () => {
|
||||||
|
bench('native-monad some(i)', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) some(i);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('ts-results-es Some(i)', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) TsSome(i);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('oxide.ts Some(i)', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) OxSome(i);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('fp-ts O.some(i)', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) O.some(i);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('true-myth Maybe.just(i)', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) TmMaybe.just(i);
|
||||||
|
}, QUICK);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Memory: 1000x Err construction', () => {
|
||||||
|
bench('native-monad err(i)', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) err(`err-${i}`);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('neverthrow err(i)', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) ntErr(`err-${i}`);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('ts-results-es Err(i)', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) TsErr(`err-${i}`);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('oxide.ts Err(i)', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) OxErr(`err-${i}`);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('fp-ts E.left(i)', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) E.left(`err-${i}`);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('true-myth Result.err(i)', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) TmResult.err(`err-${i}`);
|
||||||
|
}, QUICK);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Three-state: OkSome vs nested allocation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Memory: 1000x OkSome (single vs nested allocation)', () => {
|
||||||
|
bench('native-monad okSome(i) — single class', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) okSome(i);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('fp-ts E.right(O.some(i)) — two objects', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) E.right(O.some(i));
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('ts-results-es Ok(Some(i)) — two classes', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) TsOk(TsSome(i));
|
||||||
|
}, QUICK);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Memory: 1000x ErrNone (single vs nested allocation)', () => {
|
||||||
|
bench('native-monad errNone(i) — single class', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) errNone(`err-${i}`);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('fp-ts E.left(i) — single object', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) E.left(`err-${i}`);
|
||||||
|
}, QUICK);
|
||||||
|
|
||||||
|
bench('ts-results-es Err(i) — single class', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) TsErr(`err-${i}`);
|
||||||
|
}, QUICK);
|
||||||
|
});
|
||||||
127
benchmarks/scenarios/parsing.bench.ts
Normal file
127
benchmarks/scenarios/parsing.bench.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
ok,
|
||||||
|
err,
|
||||||
|
Result,
|
||||||
|
ntOk,
|
||||||
|
ntErr,
|
||||||
|
ntFromThrowable,
|
||||||
|
TsOk,
|
||||||
|
TsErr,
|
||||||
|
OxOk,
|
||||||
|
OxErr,
|
||||||
|
E,
|
||||||
|
pipe,
|
||||||
|
TmResult,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
const validJson = '{"name":"Alice","age":30,"email":"alice@example.com"}';
|
||||||
|
const invalidJson = 'not json {{{';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// JSON parsing — valid input
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Scenario: JSON parse + extract field — valid input', () => {
|
||||||
|
bench('native-monad', () => {
|
||||||
|
Result.from(() => JSON.parse(validJson), () => 'parse error')
|
||||||
|
.map(obj => obj.name as string)
|
||||||
|
.flatMap(name => (name.length > 0 ? ok(name) : err('empty name')));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('neverthrow', () => {
|
||||||
|
ntFromThrowable(() => JSON.parse(validJson), () => 'parse error')()
|
||||||
|
.map(obj => obj.name as string)
|
||||||
|
.andThen(name => (name.length > 0 ? ntOk(name) : ntErr('empty name')));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('ts-results-es', () => {
|
||||||
|
let r;
|
||||||
|
try { r = TsOk(JSON.parse(validJson)); }
|
||||||
|
catch { r = TsErr('parse error'); }
|
||||||
|
r.map((obj: any) => obj.name as string)
|
||||||
|
.andThen((name: string) => (name.length > 0 ? TsOk(name) : TsErr('empty name')));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('oxide.ts', () => {
|
||||||
|
let r;
|
||||||
|
try { r = OxOk(JSON.parse(validJson)); }
|
||||||
|
catch { r = OxErr('parse error'); }
|
||||||
|
r.map((obj: any) => obj.name as string)
|
||||||
|
.andThen((name: string) => (name.length > 0 ? OxOk(name) : OxErr('parse error')));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts', () => {
|
||||||
|
pipe(
|
||||||
|
E.tryCatch(() => JSON.parse(validJson), () => 'parse error'),
|
||||||
|
E.map(obj => obj.name as string),
|
||||||
|
E.flatMap(name => (name.length > 0 ? E.right(name) : E.left('empty name'))),
|
||||||
|
);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('true-myth', () => {
|
||||||
|
TmResult.tryOr('parse error', () => JSON.parse(validJson))
|
||||||
|
.map(obj => obj.name as string)
|
||||||
|
.andThen(name => (name.length > 0 ? TmResult.ok(name) : TmResult.err('empty name')));
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// JSON parsing — invalid input
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Scenario: JSON parse + extract field — invalid input', () => {
|
||||||
|
bench('native-monad', () => {
|
||||||
|
Result.from(() => JSON.parse(invalidJson), () => 'parse error')
|
||||||
|
.map(obj => obj.name as string)
|
||||||
|
.flatMap(name => (name.length > 0 ? ok(name) : err('empty name')));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('neverthrow', () => {
|
||||||
|
ntFromThrowable(() => JSON.parse(invalidJson), () => 'parse error')()
|
||||||
|
.map(obj => obj.name as string)
|
||||||
|
.andThen(name => (name.length > 0 ? ntOk(name) : ntErr('empty name')));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts', () => {
|
||||||
|
pipe(
|
||||||
|
E.tryCatch(() => JSON.parse(invalidJson), () => 'parse error'),
|
||||||
|
E.map(obj => obj.name as string),
|
||||||
|
E.flatMap(name => (name.length > 0 ? E.right(name) : E.left('empty name'))),
|
||||||
|
);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('true-myth', () => {
|
||||||
|
TmResult.tryOr('parse error', () => JSON.parse(invalidJson))
|
||||||
|
.map(obj => obj.name as string)
|
||||||
|
.andThen(name => (name.length > 0 ? TmResult.ok(name) : TmResult.err('empty name')));
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Number parsing — valid
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Scenario: number parsing — valid', () => {
|
||||||
|
const input = '42.5';
|
||||||
|
|
||||||
|
bench('native-monad', () => {
|
||||||
|
Result.from(() => {
|
||||||
|
const n = Number(input);
|
||||||
|
if (Number.isNaN(n)) throw new Error('NaN');
|
||||||
|
return n;
|
||||||
|
}).flatMap(n => (n > 0 ? ok(n) : err('not positive')));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts', () => {
|
||||||
|
pipe(
|
||||||
|
E.tryCatch(() => {
|
||||||
|
const n = Number(input);
|
||||||
|
if (Number.isNaN(n)) throw new Error('NaN');
|
||||||
|
return n;
|
||||||
|
}, () => 'parse error'),
|
||||||
|
E.flatMap(n => (n > 0 ? E.right(n) : E.left('not positive'))),
|
||||||
|
);
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
148
benchmarks/scenarios/validation-pipeline.bench.ts
Normal file
148
benchmarks/scenarios/validation-pipeline.bench.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { STANDARD } from '../helpers/bench-options.js';
|
||||||
|
import {
|
||||||
|
ok,
|
||||||
|
err,
|
||||||
|
ntOk,
|
||||||
|
ntErr,
|
||||||
|
TsOk,
|
||||||
|
TsErr,
|
||||||
|
OxOk,
|
||||||
|
OxErr,
|
||||||
|
E,
|
||||||
|
pipe,
|
||||||
|
TmResult,
|
||||||
|
} from '../helpers/imports.js';
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
age: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validUser: User = { name: 'Alice', email: 'alice@example.com', age: 30 };
|
||||||
|
const invalidUser: User = { name: '', email: 'alice@example.com', age: 30 };
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 5-check validation — all pass
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Scenario: 5-check validation pipeline — all pass', () => {
|
||||||
|
bench('native-monad', () => {
|
||||||
|
ok<User, string>(validUser)
|
||||||
|
.flatMap(u => (u.name ? ok(u) : err('name required')))
|
||||||
|
.flatMap(u => (u.email.includes('@') ? ok(u) : err('invalid email')))
|
||||||
|
.flatMap(u => (u.age >= 18 ? ok(u) : err('must be 18+')))
|
||||||
|
.flatMap(u => (u.age <= 120 ? ok(u) : err('unrealistic age')))
|
||||||
|
.map(u => ({ ...u, validated: true }));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('neverthrow', () => {
|
||||||
|
ntOk<User, string>(validUser)
|
||||||
|
.andThen(u => (u.name ? ntOk(u) : ntErr('name required')))
|
||||||
|
.andThen(u => (u.email.includes('@') ? ntOk(u) : ntErr('invalid email')))
|
||||||
|
.andThen(u => (u.age >= 18 ? ntOk(u) : ntErr('must be 18+')))
|
||||||
|
.andThen(u => (u.age <= 120 ? ntOk(u) : ntErr('unrealistic age')))
|
||||||
|
.map(u => ({ ...u, validated: true }));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('ts-results-es', () => {
|
||||||
|
TsOk<User, string>(validUser)
|
||||||
|
.andThen(u => (u.name ? TsOk(u) : TsErr('name required')))
|
||||||
|
.andThen(u => (u.email.includes('@') ? TsOk(u) : TsErr('invalid email')))
|
||||||
|
.andThen(u => (u.age >= 18 ? TsOk(u) : TsErr('must be 18+')))
|
||||||
|
.andThen(u => (u.age <= 120 ? TsOk(u) : TsErr('unrealistic age')))
|
||||||
|
.map(u => ({ ...u, validated: true }));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('oxide.ts', () => {
|
||||||
|
OxOk<User, string>(validUser)
|
||||||
|
.andThen(u => (u.name ? OxOk(u) : OxErr('name required')))
|
||||||
|
.andThen(u => (u.email.includes('@') ? OxOk(u) : OxErr('invalid email')))
|
||||||
|
.andThen(u => (u.age >= 18 ? OxOk(u) : OxErr('must be 18+')))
|
||||||
|
.andThen(u => (u.age <= 120 ? OxOk(u) : OxErr('unrealistic age')))
|
||||||
|
.map(u => ({ ...u, validated: true }));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts', () => {
|
||||||
|
pipe(
|
||||||
|
E.right<string, User>(validUser),
|
||||||
|
E.flatMap(u => (u.name ? E.right(u) : E.left('name required'))),
|
||||||
|
E.flatMap(u => (u.email.includes('@') ? E.right(u) : E.left('invalid email'))),
|
||||||
|
E.flatMap(u => (u.age >= 18 ? E.right(u) : E.left('must be 18+'))),
|
||||||
|
E.flatMap(u => (u.age <= 120 ? E.right(u) : E.left('unrealistic age'))),
|
||||||
|
E.map(u => ({ ...u, validated: true })),
|
||||||
|
);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('true-myth', () => {
|
||||||
|
TmResult.ok<User, string>(validUser)
|
||||||
|
.andThen(u => (u.name ? TmResult.ok(u) : TmResult.err('name required')))
|
||||||
|
.andThen(u => (u.email.includes('@') ? TmResult.ok(u) : TmResult.err('invalid email')))
|
||||||
|
.andThen(u => (u.age >= 18 ? TmResult.ok(u) : TmResult.err('must be 18+')))
|
||||||
|
.andThen(u => (u.age <= 120 ? TmResult.ok(u) : TmResult.err('unrealistic age')))
|
||||||
|
.map(u => ({ ...u, validated: true }));
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 5-check validation — fails at step 1 (early exit)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Scenario: 5-check validation pipeline — fails at step 1', () => {
|
||||||
|
bench('native-monad', () => {
|
||||||
|
ok<User, string>(invalidUser)
|
||||||
|
.flatMap(u => (u.name ? ok(u) : err('name required')))
|
||||||
|
.flatMap(u => (u.email.includes('@') ? ok(u) : err('invalid email')))
|
||||||
|
.flatMap(u => (u.age >= 18 ? ok(u) : err('must be 18+')))
|
||||||
|
.flatMap(u => (u.age <= 120 ? ok(u) : err('unrealistic age')))
|
||||||
|
.map(u => ({ ...u, validated: true }));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('neverthrow', () => {
|
||||||
|
ntOk<User, string>(invalidUser)
|
||||||
|
.andThen(u => (u.name ? ntOk(u) : ntErr('name required')))
|
||||||
|
.andThen(u => (u.email.includes('@') ? ntOk(u) : ntErr('invalid email')))
|
||||||
|
.andThen(u => (u.age >= 18 ? ntOk(u) : ntErr('must be 18+')))
|
||||||
|
.andThen(u => (u.age <= 120 ? ntOk(u) : ntErr('unrealistic age')))
|
||||||
|
.map(u => ({ ...u, validated: true }));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('ts-results-es', () => {
|
||||||
|
TsOk<User, string>(invalidUser)
|
||||||
|
.andThen(u => (u.name ? TsOk(u) : TsErr('name required')))
|
||||||
|
.andThen(u => (u.email.includes('@') ? TsOk(u) : TsErr('invalid email')))
|
||||||
|
.andThen(u => (u.age >= 18 ? TsOk(u) : TsErr('must be 18+')))
|
||||||
|
.andThen(u => (u.age <= 120 ? TsOk(u) : TsErr('unrealistic age')))
|
||||||
|
.map(u => ({ ...u, validated: true }));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('oxide.ts', () => {
|
||||||
|
OxOk<User, string>(invalidUser)
|
||||||
|
.andThen(u => (u.name ? OxOk(u) : OxErr('name required')))
|
||||||
|
.andThen(u => (u.email.includes('@') ? OxOk(u) : OxErr('invalid email')))
|
||||||
|
.andThen(u => (u.age >= 18 ? OxOk(u) : OxErr('must be 18+')))
|
||||||
|
.andThen(u => (u.age <= 120 ? OxOk(u) : OxErr('unrealistic age')))
|
||||||
|
.map(u => ({ ...u, validated: true }));
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('fp-ts', () => {
|
||||||
|
pipe(
|
||||||
|
E.right<string, User>(invalidUser),
|
||||||
|
E.flatMap(u => (u.name ? E.right(u) : E.left('name required'))),
|
||||||
|
E.flatMap(u => (u.email.includes('@') ? E.right(u) : E.left('invalid email'))),
|
||||||
|
E.flatMap(u => (u.age >= 18 ? E.right(u) : E.left('must be 18+'))),
|
||||||
|
E.flatMap(u => (u.age <= 120 ? E.right(u) : E.left('unrealistic age'))),
|
||||||
|
E.map(u => ({ ...u, validated: true })),
|
||||||
|
);
|
||||||
|
}, STANDARD);
|
||||||
|
|
||||||
|
bench('true-myth', () => {
|
||||||
|
TmResult.ok<User, string>(invalidUser)
|
||||||
|
.andThen(u => (u.name ? TmResult.ok(u) : TmResult.err('name required')))
|
||||||
|
.andThen(u => (u.email.includes('@') ? TmResult.ok(u) : TmResult.err('invalid email')))
|
||||||
|
.andThen(u => (u.age >= 18 ? TmResult.ok(u) : TmResult.err('must be 18+')))
|
||||||
|
.andThen(u => (u.age <= 120 ? TmResult.ok(u) : TmResult.err('unrealistic age')))
|
||||||
|
.map(u => ({ ...u, validated: true }));
|
||||||
|
}, STANDARD);
|
||||||
|
});
|
||||||
14
benchmarks/tsconfig.json
Normal file
14
benchmarks/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2022",
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["es2022"],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["**/*.bench.ts", "helpers/**/*.ts"]
|
||||||
|
}
|
||||||
9
benchmarks/vitest.config.ts
Normal file
9
benchmarks/vitest.config.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
benchmark: {
|
||||||
|
include: ['**/*.bench.ts'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
14
eslint.config.mjs
Normal file
14
eslint.config.mjs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist', 'coverage', 'node_modules'] },
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-namespace': 'off',
|
||||||
|
'max-len': ['warn', { code: 120 }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
62
package.json
Normal file
62
package.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"name": "native-monad",
|
||||||
|
"version": "0.0.5",
|
||||||
|
"description": "TypeScript monad library using native JS/TS idioms - Result, Option, and Query types",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.cjs",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/index.d.cts",
|
||||||
|
"default": "./dist/index.cjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"test": "vitest run --project unit",
|
||||||
|
"test:types": "vitest run --project types",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
|
"prepublishOnly": "rm -rf dist && tsup"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12",
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"eslint": "^9.0.0",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"tsup": "^8.0.0",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"typescript-eslint": "^8.0.0",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"monad",
|
||||||
|
"result",
|
||||||
|
"option",
|
||||||
|
"typescript",
|
||||||
|
"functional"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"sideEffects": false,
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"esbuild"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
2289
pnpm-lock.yaml
generated
Normal file
2289
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
src/index.ts
Normal file
3
src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './result';
|
||||||
|
export * from './option';
|
||||||
|
export * from './query';
|
||||||
533
src/option/index.ts
Normal file
533
src/option/index.ts
Normal file
@@ -0,0 +1,533 @@
|
|||||||
|
import { isNullish } from '../util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T The type of the contained value.
|
||||||
|
*/
|
||||||
|
export interface OptionInterface<T> {
|
||||||
|
/**
|
||||||
|
* Determines whether this option contains a value.
|
||||||
|
* @returns `true` if this option contains a value, `false` otherwise.
|
||||||
|
* ```ts
|
||||||
|
* some(42).isSome() // true
|
||||||
|
* none().isSome() // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
isSome(): this is Some<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether this option is empty.
|
||||||
|
* @returns `true` if this option is empty, `false` otherwise.
|
||||||
|
* ```ts
|
||||||
|
* some(42).isNone() // false
|
||||||
|
* none().isNone() // true
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
isNone(): this is None;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the contained value by applying a function to it.
|
||||||
|
* @param fn Function to apply to the contained value.
|
||||||
|
* @returns A new option containing the transformed value, or None if this option is empty.
|
||||||
|
* @remarks When called on a known `Some<T>`, returns `Some<U>`. When called on a known `None`, returns `None` — the function is never invoked.
|
||||||
|
* ```ts
|
||||||
|
* some(5).map(x => x * 2) // Some(10)
|
||||||
|
* none().map(x => x * 2) // None
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
map<U>(fn: (value: T) => U): Option<U>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a function that returns an option to the contained value and flattens the result.
|
||||||
|
* @param fn Function that takes the contained value and returns an option.
|
||||||
|
* @returns The option returned by the function, or None if this option is empty.
|
||||||
|
* ```ts
|
||||||
|
* some(5).flatMap(x => some(x * 2)) // Some(10)
|
||||||
|
* some(5).flatMap(x => none()) // None
|
||||||
|
* none().flatMap(x => some(x * 2)) // None
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
flatMap<U>(fn: (value: T) => Option<U>): Option<U>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes one of two functions based on whether this option contains a value.
|
||||||
|
* @param onSome Function to execute if this option contains a value.
|
||||||
|
* @param onNone Function to execute if this option is empty.
|
||||||
|
* @returns The return value of whichever function is executed. `U` is inferred as the union of both callback return types.
|
||||||
|
* ```ts
|
||||||
|
* some(42).match(x => `Value: ${x}`, () => 'No value') // 'Value: 42'
|
||||||
|
* none().match(x => `Value: ${x}`, () => 'No value') // 'No value'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
match<U>(onSome: (value: T) => U, onNone: () => U): U;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters this option based on a type predicate, narrowing the value type on success.
|
||||||
|
* @param predicate Type predicate function to test the contained value.
|
||||||
|
* @returns Some with the narrowed type if the predicate passes, None otherwise. A no-op on None.
|
||||||
|
* @remarks This overload resolves when the predicate is a type guard (`value is U`), narrowing `T` to `U`.
|
||||||
|
* ```ts
|
||||||
|
* some(42 as number | string).filter((x): x is number => typeof x === 'number') // Some(42): Option<number>
|
||||||
|
* some('hello' as number | string).filter((x): x is number => typeof x === 'number') // None
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
filter<U extends T>(predicate: (value: T) => value is U): Option<U>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters this option based on a boolean predicate.
|
||||||
|
* @param predicate Function to test the contained value.
|
||||||
|
* @returns Some if the value satisfies the predicate, None otherwise. A no-op on None.
|
||||||
|
* @remarks This overload resolves when the predicate returns `boolean` (not a type guard). The value type `T` is preserved.
|
||||||
|
* ```ts
|
||||||
|
* some(5).filter(x => x > 3) // Some(5)
|
||||||
|
* some(2).filter(x => x > 3) // None
|
||||||
|
* none().filter(x => x > 3) // None
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
filter(predicate: (value: T) => boolean): Option<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the contained value or null.
|
||||||
|
* @returns The contained value if this option is Some, null otherwise.
|
||||||
|
* ```ts
|
||||||
|
* some(42).toNullable() // 42
|
||||||
|
* none().toNullable() // null
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
toNullable(): T | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the contained value or undefined.
|
||||||
|
* @returns The contained value if this option is Some, undefined otherwise.
|
||||||
|
* ```ts
|
||||||
|
* some(42).toUndefined() // 42
|
||||||
|
* none().toUndefined() // undefined
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
toUndefined(): T | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string representation of this option.
|
||||||
|
* @returns A string indicating whether this option is Some or None, and the contained value if applicable.
|
||||||
|
* ```ts
|
||||||
|
* some(42).toString() // 'Some(42)'
|
||||||
|
* none().toString() // 'None()'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an option containing a value of type `T`.
|
||||||
|
*
|
||||||
|
* All methods that operate on the value (`map`, `flatMap`, `filter`) invoke their callbacks.
|
||||||
|
* @remarks The contained value is accessible via the `.value` property, either directly or after narrowing with `isSome()`.
|
||||||
|
*/
|
||||||
|
export class Some<T> implements OptionInterface<T> {
|
||||||
|
constructor(readonly value: T) {}
|
||||||
|
|
||||||
|
isSome(): this is Some<T> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNone(): this is never {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
map<U>(fn: (value: T) => U): Some<U> {
|
||||||
|
return some(fn(this.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
flatMap<R extends Option<any>>(fn: (value: T) => R): R {
|
||||||
|
return fn(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
match<U>(onSome: (value: T) => U, onNone: () => U): U {
|
||||||
|
return onSome(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
filter<U extends T>(predicate: (value: T) => value is U): Option<U>;
|
||||||
|
filter(predicate: (value: T) => boolean): Option<T>;
|
||||||
|
filter(predicate: (value: T) => boolean): Option<T> {
|
||||||
|
return predicate(this.value) ? this : none();
|
||||||
|
}
|
||||||
|
|
||||||
|
toNullable(): T {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
toUndefined(): T {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `Some(${this.value})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an empty option containing no value.
|
||||||
|
*
|
||||||
|
* All transformation methods (`map`, `flatMap`, `filter`) are no-ops that return the `None` singleton
|
||||||
|
* without invoking their callbacks. Instantiated via `none()`, which always returns the same frozen instance.
|
||||||
|
*/
|
||||||
|
export class None implements OptionInterface<any> {
|
||||||
|
isSome(): this is never {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNone(): this is None {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
map<U>(): None {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
flatMap(): this {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
match<U>(onSome: (value: never) => U, onNone: () => U): U {
|
||||||
|
return onNone();
|
||||||
|
}
|
||||||
|
|
||||||
|
filter(): None {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
toNullable(): null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
toUndefined(): undefined {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return 'None()';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an optional value that is either Some containing a value or None.
|
||||||
|
* @remarks Use `isSome()` or `isNone()` to check the option type and access the contained value via `.value`.
|
||||||
|
* ```ts
|
||||||
|
* const option = some(42);
|
||||||
|
* if (option.isSome()) {
|
||||||
|
* console.log(option.value); // 42
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const empty = none();
|
||||||
|
* if (empty.isNone()) {
|
||||||
|
* console.log('No value');
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export type Option<T> = Some<T> | None;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a promise that resolves to an Option.
|
||||||
|
*/
|
||||||
|
export type OptionPromise<T> = Promise<Option<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a promise that resolves to Some.
|
||||||
|
*/
|
||||||
|
export type SomePromise<T> = Promise<Some<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a promise that resolves to None.
|
||||||
|
*/
|
||||||
|
export type NonePromise = Promise<None>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the result of partitioning an array of options into values and none count.
|
||||||
|
*/
|
||||||
|
export type OptionPartitionResult<T> = { values: T[]; noneCount: number };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditional type that maps a value type to its `Option.from()` return type.
|
||||||
|
*
|
||||||
|
* When `T` is only `null | undefined`, evaluates to `None`. Otherwise evaluates to `Option<NonNullable<T>>`,
|
||||||
|
* where `NonNullable` strips `null` and `undefined` from the union.
|
||||||
|
*/
|
||||||
|
export type OptionFromType<T> = [NonNullable<T>] extends [never] ? None : Option<NonNullable<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an option from a value, treating null/undefined as None.
|
||||||
|
*/
|
||||||
|
const optionFromValue = <T>(value: T): OptionFromType<T> => {
|
||||||
|
return isNullish(value)
|
||||||
|
? (none() as OptionFromType<T>)
|
||||||
|
: (some(value as NonNullable<T>) as OptionFromType<T>);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an option containing the specified value.
|
||||||
|
* @param value The value to wrap in an option.
|
||||||
|
* @returns A Some containing the value.
|
||||||
|
* ```ts
|
||||||
|
* const option = some(42);
|
||||||
|
* if (option.isSome()) {
|
||||||
|
* console.log(option.value); // 42
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const some = <T>(value: T): Some<T> => new Some(value);
|
||||||
|
|
||||||
|
const NONE_INSTANCE = Object.freeze(new None());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the singleton None instance representing an empty option.
|
||||||
|
* @returns The None instance.
|
||||||
|
* ```ts
|
||||||
|
* none() // None()
|
||||||
|
* none() === none() // true (same instance)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const none = (): None => NONE_INSTANCE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an option from a nullable value or by executing a function.
|
||||||
|
* @param valueOrFn The value to convert to an option, or a function to execute whose return value is converted.
|
||||||
|
* @returns Some containing the value if it is not null or undefined, None otherwise.
|
||||||
|
* @remarks Only null and undefined are considered falsy. Empty strings, 0, and false create Some instances.
|
||||||
|
* When passed a function, executes it and wraps the return value — errors are not caught (Option has no error channel).
|
||||||
|
* ```ts
|
||||||
|
* Option.from(42) // Some(42)
|
||||||
|
* Option.from(0) // Some(0)
|
||||||
|
* Option.from(false) // Some(false)
|
||||||
|
* Option.from("") // Some("") — empty string is not considered falsy
|
||||||
|
* Option.from("hello" as string | null) // Some("hello") or None depending on runtime value — typed as Option<string>
|
||||||
|
* Option.from(null) // None
|
||||||
|
* Option.from(undefined) // None
|
||||||
|
* Option.from(() => 42) // Some(42) — function overload
|
||||||
|
* Option.from(() => null) // None — function overload
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @internal Type-level guard: use `OptionPromise.from()` for async functions.
|
||||||
|
*/
|
||||||
|
function optionFrom(fn: () => PromiseLike<any>): 'ERROR: Option.from() is synchronous — use OptionPromise.from() for async functions';
|
||||||
|
function optionFrom<T>(fn: () => T): OptionFromType<T>;
|
||||||
|
function optionFrom<T>(value: T): OptionFromType<T>;
|
||||||
|
function optionFrom<T>(valueOrFn: T | (() => T)): any {
|
||||||
|
const value = typeof valueOrFn === 'function' ? (valueOrFn as () => T)() : valueOrFn;
|
||||||
|
return optionFromValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines an array of options into a single option containing an array of values.
|
||||||
|
* @param options The array of options to combine.
|
||||||
|
* @returns Some containing an array of all values if all options are Some, None otherwise.
|
||||||
|
* @remarks Short-circuits on the first None encountered — remaining options are not evaluated. When all inputs are known `Some`, the return type narrows to `Some<T[]>`.
|
||||||
|
* ```ts
|
||||||
|
* Option.all([some(1), some(2), some(3)]) // Some([1, 2, 3])
|
||||||
|
* Option.all([some(1), none(), some(3)]) // None
|
||||||
|
* Option.all([]) // Some([])
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
function optionAll<T>(options: Some<T>[]): Some<T[]>;
|
||||||
|
function optionAll<T>(options: Option<T>[]): Option<T[]>;
|
||||||
|
function optionAll<T>(options: Option<T>[]): Option<T[]> {
|
||||||
|
const values = new Array<T>(options.length);
|
||||||
|
for (let i = 0; i < options.length; i++) {
|
||||||
|
const option = options[i];
|
||||||
|
if (option.isNone()) return none();
|
||||||
|
values[i] = option.value;
|
||||||
|
}
|
||||||
|
return some(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines an array of option promises into a single promise of an option containing an array of values.
|
||||||
|
* @param promises The array of option promises to combine.
|
||||||
|
* @returns A promise that resolves to Some containing an array of all values if all options are Some, None otherwise.
|
||||||
|
* @remarks Rejects if any input promise rejects. When all inputs are known `Promise<Some>`, the return type narrows to `Promise<Some<T[]>>`.
|
||||||
|
* ```ts
|
||||||
|
* await OptionPromise.all([Promise.resolve(some(1)), Promise.resolve(some(2))]) // Some([1, 2])
|
||||||
|
* await OptionPromise.all([Promise.resolve(some(1)), Promise.resolve(none())]) // None
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async function optionPromiseAll<T>(promises: Promise<Some<T>>[]): Promise<Some<T[]>>;
|
||||||
|
async function optionPromiseAll<T>(promises: Promise<Option<T>>[]): Promise<Option<T[]>>;
|
||||||
|
async function optionPromiseAll<T>(promises: Promise<Option<T>>[]): Promise<Option<T[]>> {
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
return optionAll(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace providing static utility methods for working with collections of options.
|
||||||
|
*/
|
||||||
|
export const Option = {
|
||||||
|
from: optionFrom,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a value is an Option.
|
||||||
|
* @param value The value to test.
|
||||||
|
* @returns `true` if the value is an Option, `false` otherwise.
|
||||||
|
* @remarks This is a runtime `instanceof` check — it narrows to `Option<unknown>` by default. The generic parameter `T` can be specified explicitly (e.g., `Option.isOption<number>(x)`) but is caller-asserted, not runtime-validated.
|
||||||
|
* ```ts
|
||||||
|
* Option.isOption(some(42)) // true
|
||||||
|
* Option.isOption(none()) // true
|
||||||
|
* Option.isOption(42) // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
isOption<T>(value: unknown): value is Option<T> {
|
||||||
|
return value instanceof Some || value instanceof None;
|
||||||
|
},
|
||||||
|
|
||||||
|
all: optionAll,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Partitions an array of options into values and a count of None instances.
|
||||||
|
* @param options The array of options to partition.
|
||||||
|
* @returns An object containing the extracted values and the count of None instances.
|
||||||
|
* ```ts
|
||||||
|
* Option.partition([some(1), none(), some(3)]) // { values: [1, 3], noneCount: 1 }
|
||||||
|
* Option.partition([some(1), some(2)]) // { values: [1, 2], noneCount: 0 }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
partition<T>(options: Option<T>[]): OptionPartitionResult<T> {
|
||||||
|
const values: T[] = [];
|
||||||
|
let noneCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < options.length; i++) {
|
||||||
|
const option = options[i];
|
||||||
|
if (option.isSome()) {
|
||||||
|
values.push(option.value);
|
||||||
|
} else {
|
||||||
|
noneCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { values, noneCount };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts all values from Some instances in an array of options.
|
||||||
|
* @param options The array of options to compact.
|
||||||
|
* @returns An array containing only the values from Some instances.
|
||||||
|
* ```ts
|
||||||
|
* Option.compact([some(1), none(), some(3)]) // [1, 3]
|
||||||
|
* Option.compact([none(), none()]) // []
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
compact<T>(options: Option<T>[]): T[] {
|
||||||
|
const values: T[] = [];
|
||||||
|
for (let i = 0; i < options.length; i++) {
|
||||||
|
const opt = options[i];
|
||||||
|
if (opt.isSome()) values.push(opt.value);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether any option in an array contains a value.
|
||||||
|
* @param options The array of options to test.
|
||||||
|
* @returns `true` if at least one option is Some, `false` otherwise.
|
||||||
|
* ```ts
|
||||||
|
* Option.some([some(1), none(), some(3)]) // true
|
||||||
|
* Option.some([none(), none()]) // false
|
||||||
|
* Option.some([]) // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
some<T>(options: Option<T>[]): boolean {
|
||||||
|
return options.some((opt) => opt.isSome());
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether all options in an array contain a value.
|
||||||
|
* @param options The array of options to test.
|
||||||
|
* @returns `true` if all options are Some, `false` otherwise.
|
||||||
|
* ```ts
|
||||||
|
* Option.every([some(1), some(2)]) // true
|
||||||
|
* Option.every([some(1), none()]) // false
|
||||||
|
* Option.every([]) // true (empty array is considered valid)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
every<T>(options: Option<T>[]): boolean {
|
||||||
|
return options.every((opt) => opt.isSome());
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace providing async utility methods for working with collections of option promises.
|
||||||
|
*/
|
||||||
|
export const OptionPromise = {
|
||||||
|
/**
|
||||||
|
* Creates an option promise by evaluating an async function and wrapping the resolved value.
|
||||||
|
* @param fn Function to execute that returns a promise.
|
||||||
|
* @returns A promise that resolves to Some containing the resolved value if not null/undefined, None otherwise.
|
||||||
|
* @remarks Does not catch errors. If the promise rejects, the rejection propagates.
|
||||||
|
* ```ts
|
||||||
|
* await OptionPromise.from(() => Promise.resolve(42)) // Some(42)
|
||||||
|
* await OptionPromise.from(() => Promise.resolve(null)) // None
|
||||||
|
* await OptionPromise.from(() => Promise.resolve(undefined)) // None
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async from<T>(fn: () => Promise<T>): Promise<OptionFromType<T>> {
|
||||||
|
const value = await fn();
|
||||||
|
return optionFromValue(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
all: optionPromiseAll,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Partitions an array of option promises into values and a count of None instances.
|
||||||
|
* @param promises The array of option promises to partition.
|
||||||
|
* @returns A promise that resolves to an object containing the extracted values and the count of None instances.
|
||||||
|
* @remarks Rejects if any input promise rejects.
|
||||||
|
* ```ts
|
||||||
|
* await OptionPromise.partition([Promise.resolve(some(1)), Promise.resolve(none())]) // { values: [1], noneCount: 1 }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async partition<T>(promises: Promise<Option<T>>[]): Promise<OptionPartitionResult<T>> {
|
||||||
|
const resolved = await Promise.all(promises);
|
||||||
|
return Option.partition(resolved);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts all values from Some instances in an array of option promises.
|
||||||
|
* @param promises The array of option promises to compact.
|
||||||
|
* @returns A promise that resolves to an array containing only the values from Some instances.
|
||||||
|
* @remarks Rejects if any input promise rejects.
|
||||||
|
* ```ts
|
||||||
|
* await OptionPromise.compact([Promise.resolve(some(1)), Promise.resolve(none())]) // [1]
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async compact<T>(promises: Promise<Option<T>>[]): Promise<T[]> {
|
||||||
|
const resolved = await Promise.all(promises);
|
||||||
|
return Option.compact(resolved);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether any option promise in an array resolves to a value.
|
||||||
|
* @param promises The array of option promises to test.
|
||||||
|
* @returns A promise that resolves to `true` if at least one option is Some, `false` otherwise.
|
||||||
|
* @remarks Rejects if any input promise rejects.
|
||||||
|
* ```ts
|
||||||
|
* await OptionPromise.some([Promise.resolve(some(1)), Promise.resolve(none())]) // true
|
||||||
|
* await OptionPromise.some([Promise.resolve(none()), Promise.resolve(none())]) // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async some<T>(promises: Promise<Option<T>>[]): Promise<boolean> {
|
||||||
|
const resolved = await Promise.all(promises);
|
||||||
|
return Option.some(resolved);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether all option promises in an array resolve to values.
|
||||||
|
* @param promises The array of option promises to test.
|
||||||
|
* @returns A promise that resolves to `true` if all options are Some, `false` otherwise.
|
||||||
|
* @remarks Rejects if any input promise rejects.
|
||||||
|
* ```ts
|
||||||
|
* await OptionPromise.every([Promise.resolve(some(1)), Promise.resolve(some(2))]) // true
|
||||||
|
* await OptionPromise.every([Promise.resolve(some(1)), Promise.resolve(none())]) // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async every<T>(promises: Promise<Option<T>>[]): Promise<boolean> {
|
||||||
|
const resolved = await Promise.all(promises);
|
||||||
|
return Option.every(resolved);
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
285
src/option/spec/filter.spec.ts
Normal file
285
src/option/spec/filter.spec.ts
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Option, Some, None, some, none } from '../';
|
||||||
|
|
||||||
|
describe('Option.filter - Runtime Tests', () => {
|
||||||
|
describe('filter with Some', () => {
|
||||||
|
it('should return Some when predicate returns true', () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
const predicate = vi.fn((x: number) => x > 0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = someOption.filter(predicate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate).toHaveBeenCalledWith(42);
|
||||||
|
expect(predicate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return None when predicate returns false', () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42) as Option<number>;
|
||||||
|
const predicate = vi.fn((x: number) => x < 0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = someOption.filter(predicate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate).toHaveBeenCalledWith(42);
|
||||||
|
expect(predicate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with various predicate conditions', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
{ value: 5, predicate: (x: number) => x > 3, expected: true },
|
||||||
|
{ value: 2, predicate: (x: number) => x > 3, expected: false },
|
||||||
|
{ value: 'hello', predicate: (x: string) => x.length > 3, expected: true },
|
||||||
|
{ value: 'hi', predicate: (x: string) => x.length > 3, expected: false },
|
||||||
|
{ value: [1, 2, 3], predicate: (x: number[]) => x.length > 0, expected: true },
|
||||||
|
{ value: [], predicate: (x: number[]) => x.length > 0, expected: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ value, predicate, expected }) => {
|
||||||
|
// Arrange
|
||||||
|
const option = some(value);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
// @ts-expect-error // TypeScript will infer the type based on the value
|
||||||
|
const result = option.filter(predicate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if (expected) {
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe(value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex object filtering', () => {
|
||||||
|
// Arrange
|
||||||
|
const users = [
|
||||||
|
{ name: 'Alice', age: 25, active: true },
|
||||||
|
{ name: 'Bob', age: 17, active: true },
|
||||||
|
{ name: 'Charlie', age: 30, active: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
users.forEach((user) => {
|
||||||
|
// Act
|
||||||
|
const activeAdult = some(user).filter((u) => u.active && u.age >= 18);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if (user.name === 'Alice') {
|
||||||
|
expect(activeAdult.isSome()).toBe(true);
|
||||||
|
if (activeAdult.isSome()) {
|
||||||
|
expect(activeAdult.value).toBe(user);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expect(activeAdult.isNone()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle type predicate filtering', () => {
|
||||||
|
// Arrange
|
||||||
|
const mixedValues: (string | number)[] = [42, 'hello', 100, 'world'];
|
||||||
|
|
||||||
|
mixedValues.forEach((value) => {
|
||||||
|
// Arrange
|
||||||
|
const option = some(value);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberResult = option.filter((x): x is number => typeof x === 'number');
|
||||||
|
const stringResult = option.filter((x): x is string => typeof x === 'string');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
expect(numberResult.isSome()).toBe(true);
|
||||||
|
expect(stringResult.isNone()).toBe(true);
|
||||||
|
if (numberResult.isSome()) {
|
||||||
|
expect(numberResult.value).toBe(value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expect(numberResult.isNone()).toBe(true);
|
||||||
|
expect(stringResult.isSome()).toBe(true);
|
||||||
|
if (stringResult.isSome()) {
|
||||||
|
expect(stringResult.value).toBe(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null and undefined values correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const nullOption = some(null);
|
||||||
|
const undefinedOption = some(undefined);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const nullFiltered = nullOption.filter((x) => x === null);
|
||||||
|
const undefinedFiltered = undefinedOption.filter((x) => x === undefined);
|
||||||
|
const nullRejected = nullOption.filter((x) => x !== null);
|
||||||
|
const undefinedRejected = undefinedOption.filter((x) => x !== undefined);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(nullFiltered.isSome()).toBe(true);
|
||||||
|
expect(undefinedFiltered.isSome()).toBe(true);
|
||||||
|
expect(nullRejected.isNone()).toBe(true);
|
||||||
|
expect(undefinedRejected.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter with None', () => {
|
||||||
|
it('should return None without calling predicate', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption = none() as Option<number>;
|
||||||
|
const predicate = vi.fn((x: number) => x > 0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = noneOption.filter(predicate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate).not.toHaveBeenCalled();
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return None for any predicate', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption = none() as Option<string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result1 = noneOption.filter((x) => x.length > 0);
|
||||||
|
const result2 = noneOption.filter((x) => x.length === 0);
|
||||||
|
const result3 = noneOption.filter((x): x is string => typeof x === 'string');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result1.isNone()).toBe(true);
|
||||||
|
expect(result2.isNone()).toBe(true);
|
||||||
|
expect(result3.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter chaining', () => {
|
||||||
|
it('should allow chaining multiple filters', () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = someOption
|
||||||
|
.filter((x) => x > 0)
|
||||||
|
.filter((x) => x < 100)
|
||||||
|
.filter((x) => x % 2 === 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should short-circuit on first failed filter', () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
const predicate1 = vi.fn((x: number) => x > 0);
|
||||||
|
const predicate2 = vi.fn((x: number) => x < 10); // This should fail
|
||||||
|
const predicate3 = vi.fn((x: number) => x % 2 === 0); // This should not be called
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = someOption.filter(predicate1).filter(predicate2).filter(predicate3);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate1).toHaveBeenCalledWith(42);
|
||||||
|
expect(predicate2).toHaveBeenCalledWith(42);
|
||||||
|
expect(predicate3).not.toHaveBeenCalled();
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter error handling', () => {
|
||||||
|
it('should propagate errors from predicate function', () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
const error = new Error('Predicate error');
|
||||||
|
const predicate = vi.fn(() => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(() => {
|
||||||
|
someOption.filter(predicate);
|
||||||
|
}).toThrow('Predicate error');
|
||||||
|
|
||||||
|
expect(predicate).toHaveBeenCalledWith(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call predicate on None even if it would throw', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption = none() as Option<number>;
|
||||||
|
const predicate = vi.fn(() => {
|
||||||
|
throw new Error('Should not be called');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = noneOption.filter(predicate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate).not.toHaveBeenCalled();
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter with edge cases', () => {
|
||||||
|
it('should handle falsy values correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const falsyValues = [0, false, '', NaN];
|
||||||
|
|
||||||
|
falsyValues.forEach((value) => {
|
||||||
|
// Arrange
|
||||||
|
const option = some(value);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const truthyResult = option.filter((x) => !!x);
|
||||||
|
const falsyResult = option.filter((x) => !x);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(truthyResult.isNone()).toBe(true);
|
||||||
|
expect(falsyResult.isSome()).toBe(true);
|
||||||
|
if (falsyResult.isSome()) {
|
||||||
|
expect(falsyResult.value).toBe(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex nested structures', () => {
|
||||||
|
// Arrange
|
||||||
|
const complexObject = {
|
||||||
|
data: {
|
||||||
|
items: [
|
||||||
|
{ id: 1, name: 'Item 1', tags: ['tag1', 'tag2'] },
|
||||||
|
{ id: 2, name: 'Item 2', tags: ['tag3'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const option = some(complexObject);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filtered = option.filter(
|
||||||
|
(obj) => obj.data.items.length > 0 && obj.data.items.every((item) => item.tags.length > 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filtered.isSome()).toBe(true);
|
||||||
|
if (filtered.isSome()) {
|
||||||
|
expect(filtered.value).toBe(complexObject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
172
src/option/spec/filter.type-spec.ts
Normal file
172
src/option/spec/filter.type-spec.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Option, Some, None, some, none } from "../";
|
||||||
|
|
||||||
|
describe("Option.filter - Type Tests", () => {
|
||||||
|
describe("filter with type predicate", () => {
|
||||||
|
it("should narrow type with type predicate on Some", () => {
|
||||||
|
// Arrange
|
||||||
|
const someValue = some(42 as string | number);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberResult = someValue.filter((x): x is number => typeof x === 'number');
|
||||||
|
const stringResult = someValue.filter((x): x is string => typeof x === 'string');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberResult).toEqualTypeOf<Option<number>>();
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<Option<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex type predicates on Some", () => {
|
||||||
|
// Arrange
|
||||||
|
interface User { type: 'user'; name: string; }
|
||||||
|
interface Admin { type: 'admin'; name: string; permissions: string[]; }
|
||||||
|
type Person = User | Admin;
|
||||||
|
|
||||||
|
const personSome = some({ type: 'user', name: 'John' } as Person);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const adminResult = personSome.filter((p): p is Admin => p.type === 'admin');
|
||||||
|
const userResult = personSome.filter((p): p is User => p.type === 'user');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(adminResult).toEqualTypeOf<Option<Admin>>();
|
||||||
|
expectTypeOf(userResult).toEqualTypeOf<Option<User>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle nullable type predicates on Some", () => {
|
||||||
|
// Arrange
|
||||||
|
const nullableSome = some("hello" as string | null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const nonNullResult = nullableSome.filter((x): x is string => x !== null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(nonNullResult).toEqualTypeOf<Option<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle array type predicates on Some", () => {
|
||||||
|
// Arrange
|
||||||
|
const arraySome = some([1, 2, 3] as unknown[]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberArrayResult = arraySome.filter((arr): arr is number[] =>
|
||||||
|
arr.every(item => typeof item === 'number')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberArrayResult).toEqualTypeOf<Option<number[]>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter with boolean predicate", () => {
|
||||||
|
it("should preserve original type with boolean predicate", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberOption = some(42);
|
||||||
|
const stringOption = some("hello");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filteredNumber = numberOption.filter(x => x > 0);
|
||||||
|
const filteredString = stringOption.filter(x => x.length > 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(filteredNumber).toEqualTypeOf<Option<number>>();
|
||||||
|
expectTypeOf(filteredString).toEqualTypeOf<Option<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with complex boolean predicates", () => {
|
||||||
|
// Arrange
|
||||||
|
const objectOption = some({ id: 1, name: "test", active: true });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filteredObject = objectOption.filter(obj => obj.active && obj.name.length > 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(filteredObject).toEqualTypeOf<Option<{ id: number; name: string; active: boolean }>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter with None", () => {
|
||||||
|
it("should preserve None type regardless of predicate", () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption = none() as Option<number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filteredWithBoolean = noneOption.filter(x => x > 0);
|
||||||
|
const filteredWithTypePredicate = noneOption.filter((x): x is number => typeof x === 'number');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(filteredWithBoolean).toEqualTypeOf<Option<number>>();
|
||||||
|
expectTypeOf(filteredWithTypePredicate).toEqualTypeOf<Option<number>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter function signature validation", () => {
|
||||||
|
it("should be callable with correct predicate signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
const booleanPredicate = (x: number) => x > 0;
|
||||||
|
const typePredicate = (x: number): x is number => typeof x === 'number';
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(someOption.filter).toBeCallableWith(booleanPredicate);
|
||||||
|
expectTypeOf(someOption.filter).toBeCallableWith(typePredicate);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be callable with incorrect predicate signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// @ts-expect-error - wrong parameter type
|
||||||
|
someOption.filter((x: string) => x.length > 0);
|
||||||
|
// @ts-expect-error - wrong return type
|
||||||
|
someOption.filter((x: number) => x.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be callable without predicate", () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// @ts-expect-error - missing predicate argument
|
||||||
|
someOption.filter();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter overload resolution", () => {
|
||||||
|
it("should resolve to type predicate overload when type predicate is provided", () => {
|
||||||
|
// Arrange - use Some directly to test the type predicate overload
|
||||||
|
const someValue = some(42 as string | number);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = someValue.filter((x): x is number => typeof x === 'number');
|
||||||
|
|
||||||
|
// Assert - should be Option<number>, not Option<string | number>
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Option<number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should resolve to boolean predicate overload when boolean predicate is provided", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberOption = some(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = numberOption.filter(x => x > 0);
|
||||||
|
|
||||||
|
// Assert - should preserve original type
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Option<number>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter with generic constraints", () => {
|
||||||
|
it("should work with generic type constraints", () => {
|
||||||
|
// Arrange - use Some directly to test the type predicate overload
|
||||||
|
const lengthableSome = some("hello" as { length: number });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = lengthableSome.filter((x): x is string => typeof x === 'string');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Option<string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
318
src/option/spec/is.spec.ts
Normal file
318
src/option/spec/is.spec.ts
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Option, Some, None, some, none } from '../';
|
||||||
|
import {fail} from "node:assert";
|
||||||
|
|
||||||
|
describe('Option isSome/isNone - Runtime Tests', () => {
|
||||||
|
describe('isSome() method', () => {
|
||||||
|
it('should return true for Some instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
some(42),
|
||||||
|
some('hello'),
|
||||||
|
some(true),
|
||||||
|
some({ id: 1 }),
|
||||||
|
some([1, 2, 3]),
|
||||||
|
some(null),
|
||||||
|
some(undefined),
|
||||||
|
some(0),
|
||||||
|
some(false),
|
||||||
|
some('')
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(option => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(option.isSome()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for None instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption = none();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(noneOption.isSome()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable access to value property after type narrowing', () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<string> = some('hello') as Option<string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (option.isSome()) {
|
||||||
|
expect(option.value).toBe('hello');
|
||||||
|
expect(typeof option.value).toBe('string');
|
||||||
|
} else {
|
||||||
|
fail('Expected option to be Some');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with complex objects', () => {
|
||||||
|
// Arrange
|
||||||
|
const complexObject = {
|
||||||
|
id: 1,
|
||||||
|
name: 'test',
|
||||||
|
nested: { data: [1, 2, 3] },
|
||||||
|
fn: (x: number) => x * 2
|
||||||
|
};
|
||||||
|
const option: Option<typeof complexObject> = some(complexObject) as Option<typeof complexObject>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (option.isSome()) {
|
||||||
|
expect(option.value).toBe(complexObject);
|
||||||
|
expect(option.value.id).toBe(1);
|
||||||
|
expect(option.value.name).toBe('test');
|
||||||
|
expect(option.value.nested.data).toEqual([1, 2, 3]);
|
||||||
|
expect(option.value.fn(5)).toBe(10);
|
||||||
|
} else {
|
||||||
|
fail('Expected option to be Some');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with falsy values that are still Some', () => {
|
||||||
|
// Arrange
|
||||||
|
const falsy: Option<number | boolean | string | null | undefined>[] = [
|
||||||
|
some(0) as Option<number>,
|
||||||
|
some(false) as Option<boolean>,
|
||||||
|
some('') as Option<string>,
|
||||||
|
some(null) as Option<null>,
|
||||||
|
some(undefined) as Option<undefined>
|
||||||
|
];
|
||||||
|
|
||||||
|
falsy.forEach(option => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(option.isSome()).toBe(true);
|
||||||
|
if (option.isSome()) {
|
||||||
|
// Value should be accessible even if falsy
|
||||||
|
expect(option.value !== undefined || option.value === undefined).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isNone() method', () => {
|
||||||
|
it('should return true for None instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption = none();
|
||||||
|
const noneAsOption: Option<any> = none() as Option<any>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(noneOption.isNone()).toBe(true);
|
||||||
|
expect(noneAsOption.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for Some instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
some(42),
|
||||||
|
some('hello'),
|
||||||
|
some(true),
|
||||||
|
some({ id: 1 }),
|
||||||
|
some([1, 2, 3]),
|
||||||
|
some(null),
|
||||||
|
some(undefined),
|
||||||
|
some(0),
|
||||||
|
some(false),
|
||||||
|
some('')
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(option => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(option.isNone()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with different generic types', () => {
|
||||||
|
// Arrange
|
||||||
|
const numberNone: Option<number> = none() as Option<number>;
|
||||||
|
const stringNone: Option<string> = none() as Option<string>;
|
||||||
|
const objectNone: Option<{ id: number }> = none() as Option<{ id: number }>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(numberNone.isNone()).toBe(true);
|
||||||
|
expect(stringNone.isNone()).toBe(true);
|
||||||
|
expect(objectNone.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Mutual exclusivity', () => {
|
||||||
|
it('should be mutually exclusive for Some instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
some(42),
|
||||||
|
some('hello'),
|
||||||
|
some(null),
|
||||||
|
some(undefined),
|
||||||
|
some(false),
|
||||||
|
some(0)
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(option => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(option.isSome()).toBe(true);
|
||||||
|
expect(option.isNone()).toBe(false);
|
||||||
|
expect(option.isSome() && option.isNone()).toBe(false);
|
||||||
|
// @ts-expect-error - TypeScript should not allow this
|
||||||
|
expect(option.isSome() || option.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be mutually exclusive for None instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption = none();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(noneOption.isSome()).toBe(false);
|
||||||
|
expect(noneOption.isNone()).toBe(true);
|
||||||
|
// @ts-expect-error - TypeScript should not allow this
|
||||||
|
expect(noneOption.isSome() && noneOption.isNone()).toBe(false);
|
||||||
|
expect(noneOption.isSome() || noneOption.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Type narrowing in control flow', () => {
|
||||||
|
it('should enable type-safe value access in if statements', () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<{ name: string; age: number }> = some({ name: 'John', age: 30 }) as Option<{ name: string; age: number }>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (option.isSome()) {
|
||||||
|
expect(option.value.name).toBe('John');
|
||||||
|
expect(option.value.age).toBe(30);
|
||||||
|
expect(typeof option.value.name).toBe('string');
|
||||||
|
expect(typeof option.value.age).toBe('number');
|
||||||
|
} else {
|
||||||
|
fail('Expected option to be Some');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with early returns', () => {
|
||||||
|
// Arrange
|
||||||
|
function processOption(option: Option<number>): string {
|
||||||
|
if (option.isNone()) {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
return `value: ${option.value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const someOption: Option<number> = some(42) as Option<number>;
|
||||||
|
const noneOption: Option<number> = none() as Option<number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const someResult = processOption(someOption);
|
||||||
|
const noneResult = processOption(noneOption);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(someResult).toBe('value: 42');
|
||||||
|
expect(noneResult).toBe('none');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work in complex conditional logic', () => {
|
||||||
|
// Arrange
|
||||||
|
const option1: Option<number> = some(10) as Option<number>;
|
||||||
|
const option2: Option<string> = some('test') as Option<string>;
|
||||||
|
const option3: Option<boolean> = none() as Option<boolean>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (option1.isSome() && option2.isSome()) {
|
||||||
|
expect(option1.value + option2.value.length).toBe(14);
|
||||||
|
} else {
|
||||||
|
fail('Expected both options to be Some');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option1.isSome() && option3.isNone()) {
|
||||||
|
expect(option1.value).toBe(10);
|
||||||
|
} else {
|
||||||
|
fail('Expected option1 to be Some and option3 to be None');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with negated conditions', () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption: Option<string> = some('hello') as Option<string>;
|
||||||
|
const noneOption: Option<string> = none() as Option<string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (!someOption.isNone()) {
|
||||||
|
expect(someOption.value).toBe('hello');
|
||||||
|
} else {
|
||||||
|
fail('Expected option to be Some');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noneOption.isSome()) {
|
||||||
|
expect(noneOption.isNone()).toBe(true);
|
||||||
|
} else {
|
||||||
|
fail('Expected option to be None');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Performance and consistency', () => {
|
||||||
|
it('should return consistent results across multiple calls', () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
const noneOption = none();
|
||||||
|
|
||||||
|
// Act & Assert - Multiple calls should return same result
|
||||||
|
expect(someOption.isSome()).toBe(someOption.isSome());
|
||||||
|
expect(someOption.isNone()).toBe(someOption.isNone());
|
||||||
|
expect(noneOption.isSome()).toBe(noneOption.isSome());
|
||||||
|
expect(noneOption.isNone()).toBe(noneOption.isNone());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be callable without side effects', () => {
|
||||||
|
// Arrange
|
||||||
|
const option = some({ counter: 0 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const before = option.value.counter;
|
||||||
|
option.isSome();
|
||||||
|
option.isNone();
|
||||||
|
option.isSome();
|
||||||
|
const after = option.value.counter;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(before).toBe(after);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge cases', () => {
|
||||||
|
it('should handle deeply nested objects', () => {
|
||||||
|
// Arrange
|
||||||
|
const deepObject = {
|
||||||
|
level1: {
|
||||||
|
level2: {
|
||||||
|
level3: {
|
||||||
|
value: 'deep'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const option: Option<typeof deepObject> = some(deepObject) as Option<typeof deepObject>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (option.isSome()) {
|
||||||
|
expect(option.value.level1.level2.level3.value).toBe('deep');
|
||||||
|
} else {
|
||||||
|
fail('Expected option to be Some');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle arrays with mixed types', () => {
|
||||||
|
// Arrange
|
||||||
|
const mixedArray = [1, 'string', true, null, { id: 1 }];
|
||||||
|
const option: Option<typeof mixedArray> = some(mixedArray) as Option<typeof mixedArray>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (option.isSome()) {
|
||||||
|
expect(option.value).toHaveLength(5);
|
||||||
|
expect(option.value[0]).toBe(1);
|
||||||
|
expect(option.value[1]).toBe('string');
|
||||||
|
expect(option.value[2]).toBe(true);
|
||||||
|
expect(option.value[3]).toBeNull();
|
||||||
|
expect(option.value[4]).toEqual({ id: 1 });
|
||||||
|
} else {
|
||||||
|
fail('Expected option to be Some');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
206
src/option/spec/is.type-spec.ts
Normal file
206
src/option/spec/is.type-spec.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Option, Some, None, some, none } from "../";
|
||||||
|
|
||||||
|
describe("Option isSome/isNone - Type Tests", () => {
|
||||||
|
describe("isSome() type guard", () => {
|
||||||
|
it("should act as type guard narrowing Option<T> to Some<T>", () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<number> = some(42) as Option<number>;
|
||||||
|
|
||||||
|
// Act & Assert - Type narrowing should work
|
||||||
|
if (option.isSome()) {
|
||||||
|
expectTypeOf(option).toEqualTypeOf<Some<number>>();
|
||||||
|
expectTypeOf(option.value).toEqualTypeOf<number>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return type predicate 'this is Some<T>'", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberOption: Option<number> = some(42) as Option<number>;
|
||||||
|
const stringOption: Option<string> = some("hello") as Option<string>;
|
||||||
|
const objectOption: Option<{ id: number }> = some({ id: 1 }) as Option<{ id: number }>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberResult = numberOption.isSome();
|
||||||
|
const stringResult = stringOption.isSome();
|
||||||
|
const objectResult = objectOption.isSome();
|
||||||
|
|
||||||
|
// Assert - Should return boolean but enable type narrowing
|
||||||
|
expectTypeOf(numberResult).toEqualTypeOf<boolean>();
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<boolean>();
|
||||||
|
expectTypeOf(objectResult).toEqualTypeOf<boolean>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should enable access to value property after type narrowing", () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<{ name: string; age: number }> = some({ name: "John", age: 30 }) as Option<{ name: string; age: number }>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (option.isSome()) {
|
||||||
|
expectTypeOf(option.value).toEqualTypeOf<{ name: string; age: number }>();
|
||||||
|
expectTypeOf(option.value.name).toEqualTypeOf<string>();
|
||||||
|
expectTypeOf(option.value.age).toEqualTypeOf<number>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with complex generic types", () => {
|
||||||
|
// Arrange
|
||||||
|
type ComplexType = Array<{ id: number; tags: string[] }>;
|
||||||
|
const option: Option<ComplexType> = some([{ id: 1, tags: ["a", "b"] }]) as Option<ComplexType>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (option.isSome()) {
|
||||||
|
expectTypeOf(option).toEqualTypeOf<Some<ComplexType>>();
|
||||||
|
expectTypeOf(option.value).toEqualTypeOf<ComplexType>();
|
||||||
|
expectTypeOf(option.value[0]).toEqualTypeOf<{ id: number; tags: string[] }>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with union types", () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<string | number> = some("hello") as Option<string | number>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (option.isSome()) {
|
||||||
|
expectTypeOf(option).toEqualTypeOf<Some<string | number>>();
|
||||||
|
expectTypeOf(option.value).toEqualTypeOf<string | number>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with nullable types", () => {
|
||||||
|
// Arrange
|
||||||
|
const nullOption: Option<null> = some(null) as Option<null>;
|
||||||
|
const undefinedOption: Option<undefined> = some(undefined) as Option<undefined>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (nullOption.isSome()) {
|
||||||
|
expectTypeOf(nullOption).toEqualTypeOf<Some<null>>();
|
||||||
|
expectTypeOf(nullOption.value).toEqualTypeOf<null>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (undefinedOption.isSome()) {
|
||||||
|
expectTypeOf(undefinedOption).toEqualTypeOf<Some<undefined>>();
|
||||||
|
expectTypeOf(undefinedOption.value).toEqualTypeOf<undefined>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isNone() type guard", () => {
|
||||||
|
it("should act as type guard narrowing Option<T> to None", () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<number> = none() as Option<number>;
|
||||||
|
|
||||||
|
// Act & Assert - Type narrowing should work
|
||||||
|
if (option.isNone()) {
|
||||||
|
expectTypeOf(option).toEqualTypeOf<None>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return type predicate 'this is None'", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberOption: Option<number> = none() as Option<number>;
|
||||||
|
const stringOption: Option<string> = none() as Option<string>;
|
||||||
|
const objectOption: Option<{ id: number }> = none() as Option<{ id: number }>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberResult = numberOption.isNone();
|
||||||
|
const stringResult = stringOption.isNone();
|
||||||
|
const objectResult = objectOption.isNone();
|
||||||
|
|
||||||
|
// Assert - Should return boolean but enable type narrowing
|
||||||
|
expectTypeOf(numberResult).toEqualTypeOf<boolean>();
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<boolean>();
|
||||||
|
expectTypeOf(objectResult).toEqualTypeOf<boolean>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should narrow to None regardless of original generic type", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberOption: Option<number> = none() as Option<number>;
|
||||||
|
const complexOption: Option<{ data: Array<string> }> = none() as Option<{ data: Array<string> }>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (numberOption.isNone()) {
|
||||||
|
expectTypeOf(numberOption).toEqualTypeOf<None>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (complexOption.isNone()) {
|
||||||
|
expectTypeOf(complexOption).toEqualTypeOf<None>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Type narrowing patterns", () => {
|
||||||
|
it("should enable exhaustive type checking with if-else", () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<string> = some("test") as Option<string>;
|
||||||
|
|
||||||
|
// Act & Assert - Both branches should be type-safe
|
||||||
|
if (option.isSome()) {
|
||||||
|
expectTypeOf(option).toEqualTypeOf<Some<string>>();
|
||||||
|
expectTypeOf(option.value).toEqualTypeOf<string>();
|
||||||
|
} else {
|
||||||
|
expectTypeOf(option).toEqualTypeOf<None>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with early returns", () => {
|
||||||
|
// Arrange
|
||||||
|
function processOption(option: Option<number>): number {
|
||||||
|
if (option.isNone()) {
|
||||||
|
return 0; // Type narrowed to None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type narrowed to Some<number> here
|
||||||
|
return option.value * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testOption: Option<number> = some(5) as Option<number>;
|
||||||
|
const result = processOption(testOption);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<number>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work in complex conditional logic", () => {
|
||||||
|
// Arrange
|
||||||
|
const option1: Option<number> = some(1) as Option<number>;
|
||||||
|
const option2: Option<string> = some("test") as Option<string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (option1.isSome() && option2.isSome()) {
|
||||||
|
expectTypeOf(option1).toEqualTypeOf<Some<number>>();
|
||||||
|
expectTypeOf(option2).toEqualTypeOf<Some<string>>();
|
||||||
|
expectTypeOf(option1.value).toEqualTypeOf<number>();
|
||||||
|
expectTypeOf(option2.value).toEqualTypeOf<string>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with negated conditions", () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<boolean> = some(true) as Option<boolean>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (!option.isNone()) {
|
||||||
|
expectTypeOf(option).toEqualTypeOf<Some<boolean>>();
|
||||||
|
expectTypeOf(option.value).toEqualTypeOf<boolean>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!option.isSome()) {
|
||||||
|
expectTypeOf(option).toEqualTypeOf<None>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Method chaining with type narrowing", () => {
|
||||||
|
it("should preserve type information in method chains", () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<number> = some(42) as Option<number>;
|
||||||
|
|
||||||
|
// Act & Assert - Type narrowing should work before method calls
|
||||||
|
if (option.isSome()) {
|
||||||
|
const mapped = option.map(x => x.toString());
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<Some<string>>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
348
src/option/spec/map.spec.ts
Normal file
348
src/option/spec/map.spec.ts
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Option, Some, None, some, none } from '../';
|
||||||
|
|
||||||
|
describe('Option map and flatMap - Runtime Tests', () => {
|
||||||
|
describe('map method', () => {
|
||||||
|
describe('when Option is Some', () => {
|
||||||
|
it('should transform the value using the provided function', () => {
|
||||||
|
// Arrange
|
||||||
|
const option = some(5);
|
||||||
|
const mapper = (x: number) => x * 2;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.map(mapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe(10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle different type transformations', () => {
|
||||||
|
// Arrange
|
||||||
|
const numberOption = some(42);
|
||||||
|
const stringOption = some("hello");
|
||||||
|
const booleanOption = some(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberToString = numberOption.map(x => x.toString());
|
||||||
|
const stringToNumber = stringOption.map(s => s.length);
|
||||||
|
const booleanToString = booleanOption.map(b => b ? "yes" : "no");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(numberToString.isSome()).toBe(true);
|
||||||
|
expect(stringToNumber.isSome()).toBe(true);
|
||||||
|
expect(booleanToString.isSome()).toBe(true);
|
||||||
|
|
||||||
|
if (numberToString.isSome()) expect(numberToString.value).toBe("42");
|
||||||
|
if (stringToNumber.isSome()) expect(stringToNumber.value).toBe(5);
|
||||||
|
if (booleanToString.isSome()) expect(booleanToString.value).toBe("yes");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex object transformations', () => {
|
||||||
|
// Arrange
|
||||||
|
const user = { id: 1, name: "John", age: 30 };
|
||||||
|
const userOption = some(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const userSummary = userOption.map(u => ({
|
||||||
|
displayName: `${u.name} (${u.age})`,
|
||||||
|
isAdult: u.age >= 18
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(userSummary.isSome()).toBe(true);
|
||||||
|
if (userSummary.isSome()) {
|
||||||
|
expect(userSummary.value).toEqual({
|
||||||
|
displayName: "John (30)",
|
||||||
|
isAdult: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle function transformations', () => {
|
||||||
|
// Arrange
|
||||||
|
const addOne = (x: number) => x + 1;
|
||||||
|
const funcOption = some(addOne);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const wrappedFunc = funcOption.map(fn => (y: number) => fn(y) * 2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrappedFunc.isSome()).toBe(true);
|
||||||
|
if (wrappedFunc.isSome()) {
|
||||||
|
const result = wrappedFunc.value(5);
|
||||||
|
expect(result).toBe(12); // (5 + 1) * 2
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not mutate the original option', () => {
|
||||||
|
// Arrange
|
||||||
|
const original = some({ count: 5 });
|
||||||
|
const originalValue = original.value;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mapped = original.map(obj => ({ ...obj, count: obj.count * 2 }));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(original.value).toBe(originalValue);
|
||||||
|
expect(original.value.count).toBe(5);
|
||||||
|
if (mapped.isSome()) {
|
||||||
|
expect(mapped.value.count).toBe(10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Option is None', () => {
|
||||||
|
it('should return None without calling the mapper function', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption: Option<number> = none() as Option<number>;
|
||||||
|
const mapperSpy = vi.fn((x: number) => x * 2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = noneOption.map(mapperSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
expect(mapperSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve None through multiple map operations', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption: Option<number> = none() as Option<number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = noneOption
|
||||||
|
.map(x => x * 2)
|
||||||
|
.map(x => x.toString())
|
||||||
|
.map(s => s.length);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('flatMap method', () => {
|
||||||
|
describe('when Option is Some', () => {
|
||||||
|
it('should apply function and flatten the result when function returns Some', () => {
|
||||||
|
// Arrange
|
||||||
|
const option = some(5);
|
||||||
|
const flatMapper = (x: number) => some(x * 2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.flatMap(flatMapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe(10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return None when function returns None', () => {
|
||||||
|
// Arrange
|
||||||
|
const option = some(5);
|
||||||
|
const flatMapper = (x: number) => none() as Option<number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.flatMap(flatMapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle conditional logic in flatMapper', () => {
|
||||||
|
// Arrange
|
||||||
|
const positiveOption = some(5);
|
||||||
|
const negativeOption = some(-3);
|
||||||
|
const flatMapper = (x: number) =>
|
||||||
|
x > 0 ? some(x * 2) : none() as Option<number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const positiveResult = positiveOption.flatMap(flatMapper);
|
||||||
|
const negativeResult = negativeOption.flatMap(flatMapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(positiveResult.isSome()).toBe(true);
|
||||||
|
expect(negativeResult.isNone()).toBe(true);
|
||||||
|
if (positiveResult.isSome()) {
|
||||||
|
expect(positiveResult.value).toBe(10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle type transformations', () => {
|
||||||
|
// Arrange
|
||||||
|
const stringOption = some("42");
|
||||||
|
const parseNumber = (str: string): Option<number> => {
|
||||||
|
const num = parseInt(str);
|
||||||
|
return isNaN(num) ? none() as Option<number> : some(num);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const validResult = stringOption.flatMap(parseNumber);
|
||||||
|
const invalidResult = some("invalid").flatMap(parseNumber);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(validResult.isSome()).toBe(true);
|
||||||
|
expect(invalidResult.isNone()).toBe(true);
|
||||||
|
if (validResult.isSome()) {
|
||||||
|
expect(validResult.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with complex business logic', () => {
|
||||||
|
// Arrange
|
||||||
|
type User = { id: number; email: string; verified: boolean };
|
||||||
|
type AuthToken = { token: string; userId: number };
|
||||||
|
|
||||||
|
const user: User = { id: 1, email: "test@example.com", verified: true };
|
||||||
|
const userOption = some(user);
|
||||||
|
|
||||||
|
const generateToken = (u: User): Option<AuthToken> => {
|
||||||
|
return u.verified
|
||||||
|
? some({ token: `token_${u.id}`, userId: u.id })
|
||||||
|
: none() as Option<AuthToken>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const tokenResult = userOption.flatMap(generateToken);
|
||||||
|
const unverifiedResult = some({ ...user, verified: false }).flatMap(generateToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(tokenResult.isSome()).toBe(true);
|
||||||
|
expect(unverifiedResult.isNone()).toBe(true);
|
||||||
|
if (tokenResult.isSome()) {
|
||||||
|
expect(tokenResult.value).toEqual({ token: "token_1", userId: 1 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not mutate the original option', () => {
|
||||||
|
// Arrange
|
||||||
|
const original = some({ value: 10 });
|
||||||
|
const originalValue = original.value;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMapped = original.flatMap(obj => some({ ...obj, value: obj.value * 2 }));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(original.value).toBe(originalValue);
|
||||||
|
expect(original.value.value).toBe(10);
|
||||||
|
if (flatMapped.isSome()) {
|
||||||
|
expect(flatMapped.value.value).toBe(20);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Option is None', () => {
|
||||||
|
it('should return None without calling the flatMapper function', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption: Option<number> = none() as Option<number>;
|
||||||
|
const flatMapperSpy = vi.fn((x: number) => some(x * 2));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = noneOption.flatMap(flatMapperSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
expect(flatMapperSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve None through multiple flatMap operations', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption: Option<number> = none() as Option<number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = noneOption
|
||||||
|
.flatMap(x => some(x * 2))
|
||||||
|
.flatMap(x => some(x.toString()))
|
||||||
|
.flatMap(s => some(s.length));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('map and flatMap integration', () => {
|
||||||
|
it('should work together in complex chains', () => {
|
||||||
|
// Arrange
|
||||||
|
const input = some("123");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = input
|
||||||
|
.map(str => str.trim())
|
||||||
|
.flatMap(str => {
|
||||||
|
const num = parseInt(str);
|
||||||
|
return isNaN(num) ? none() as Option<number> : some(num);
|
||||||
|
})
|
||||||
|
.map(num => num * 2)
|
||||||
|
.flatMap(num => num > 100 ? some(`Large: ${num}`) : some(`Small: ${num}`))
|
||||||
|
.map(str => str.toUpperCase());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe("LARGE: 246");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should short-circuit on first None in chain', () => {
|
||||||
|
// Arrange
|
||||||
|
const mapSpy = vi.fn((x: string) => x.toUpperCase());
|
||||||
|
const flatMapSpy = vi.fn((x: string) => some(x.length));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = some("test")
|
||||||
|
.flatMap(x => none() as Option<string>) // This returns None
|
||||||
|
.map(mapSpy) // Should not be called
|
||||||
|
.flatMap(flatMapSpy); // Should not be called
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
expect(mapSpy).not.toHaveBeenCalled();
|
||||||
|
expect(flatMapSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle alternating map and flatMap operations', () => {
|
||||||
|
// Arrange
|
||||||
|
const startValue = 5;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = some(startValue)
|
||||||
|
.map(x => x * 2) // 10
|
||||||
|
.flatMap(x => x > 5 ? some(x.toString()) : none() as Option<string>) // "10"
|
||||||
|
.map(s => s.length) // 2
|
||||||
|
.flatMap(len => len > 1 ? some(`Length: ${len}`) : none() as Option<string>) // "Length: 2"
|
||||||
|
.map(s => s.split(' ')[1]); // "2"
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe("2");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle edge cases with empty/falsy values', () => {
|
||||||
|
// Arrange
|
||||||
|
const emptyStringOption = some("");
|
||||||
|
const zeroOption = some(0);
|
||||||
|
const falseOption = some(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringResult = emptyStringOption.map(s => s.length).flatMap(len => some(`Length: ${len}`));
|
||||||
|
const numberResult = zeroOption.flatMap(n => some(n.toString())).map(s => s || "zero");
|
||||||
|
const booleanResult = falseOption.map(b => !b).flatMap(b => some(b ? "true" : "false"));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(stringResult.isSome()).toBe(true);
|
||||||
|
expect(numberResult.isSome()).toBe(true);
|
||||||
|
expect(booleanResult.isSome()).toBe(true);
|
||||||
|
|
||||||
|
if (stringResult.isSome()) expect(stringResult.value).toBe("Length: 0");
|
||||||
|
if (numberResult.isSome()) expect(numberResult.value).toBe("0");
|
||||||
|
if (booleanResult.isSome()) expect(booleanResult.value).toBe("true");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
249
src/option/spec/map.type-spec.ts
Normal file
249
src/option/spec/map.type-spec.ts
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Option, Some, None, some, none } from "../";
|
||||||
|
|
||||||
|
describe("Option map and flatMap - Type Tests", () => {
|
||||||
|
describe("map method", () => {
|
||||||
|
it("should transform Some<T> to Some<U> with correct type inference", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberOption = some(42);
|
||||||
|
const stringOption = some("hello");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const doubledNumber = numberOption.map(x => x * 2);
|
||||||
|
const uppercaseString = stringOption.map(s => s.toUpperCase());
|
||||||
|
const numberToString = numberOption.map(x => x.toString());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(doubledNumber).toEqualTypeOf<Some<number>>();
|
||||||
|
expectTypeOf(uppercaseString).toEqualTypeOf<Some<string>>();
|
||||||
|
expectTypeOf(numberToString).toEqualTypeOf<Some<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should transform None to None with correct return type", () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption: Option<number> = none() as Option<number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mapped = noneOption.map(x => x * 2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<Option<number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex type transformations", () => {
|
||||||
|
// Arrange
|
||||||
|
type User = { id: number; name: string };
|
||||||
|
type UserDto = { userId: number; displayName: string };
|
||||||
|
const userOption = some({ id: 1, name: "John" } as User);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const userDtoOption = userOption.map((user): UserDto => ({
|
||||||
|
userId: user.id,
|
||||||
|
displayName: user.name.toUpperCase()
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(userDtoOption).toEqualTypeOf<Some<UserDto>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve literal types when appropriate", () => {
|
||||||
|
// Arrange
|
||||||
|
const literalOption = some("success" as const);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mappedLiteral = literalOption.map(x => x);
|
||||||
|
const transformedLiteral = literalOption.map(x => `status: ${x}` as const);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(mappedLiteral).toEqualTypeOf<Some<"success">>();
|
||||||
|
expectTypeOf(transformedLiteral).toEqualTypeOf<Some<"status: success">>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle function type transformations", () => {
|
||||||
|
// Arrange
|
||||||
|
const funcOption = some((x: number) => x * 2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const wrappedFunc = funcOption.map(fn => (y: number) => fn(y) + 1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(wrappedFunc).toEqualTypeOf<Some<(y: number) => number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with union types", () => {
|
||||||
|
// Arrange
|
||||||
|
const unionOption: Option<string | number> = some("hello" as string | number);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringified = unionOption.map(x => String(x));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringified).toEqualTypeOf<Option<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with correct mapper function signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberOption = some(42);
|
||||||
|
const stringOption = some("hello");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberOption.map).toBeCallableWith((x: number) => x * 2);
|
||||||
|
expectTypeOf(numberOption.map).toBeCallableWith((x: number) => x.toString());
|
||||||
|
expectTypeOf(stringOption.map).toBeCallableWith((s: string) => s.length);
|
||||||
|
// @ts-expect-error - wrong parameter type
|
||||||
|
numberOption.map((s: string) => s.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("narrow return types on concrete variants", () => {
|
||||||
|
it("should return Some<U> when map is called on a known Some", () => {
|
||||||
|
const mapped = some(42).map(x => x.toString());
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<Some<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return None when map is called on a known None", () => {
|
||||||
|
const mapped = none().map(x => String(x));
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<None>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return None when flatMap is called on a known None", () => {
|
||||||
|
const mapped = none().flatMap(x => some(String(x)));
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<None>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return None when filter is called on a known None", () => {
|
||||||
|
const filtered = none().filter();
|
||||||
|
expectTypeOf(filtered).toEqualTypeOf<None>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Some<U> when flatMap callback returns Some", () => {
|
||||||
|
const result = some(5).flatMap(x => some(x.toString()));
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Some<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Option<U> when flatMap callback returns Option", () => {
|
||||||
|
const result = some(5).flatMap(x => (x > 0 ? some(x) : none()) as Option<number>);
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Option<number>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("flatMap method", () => {
|
||||||
|
it("should flatten Some<T> with function returning Option<U>", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberOption = some(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMappedSome = numberOption.flatMap(x => some(x * 2));
|
||||||
|
const flatMappedNone = numberOption.flatMap(x => none() as Option<number>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(flatMappedSome).toEqualTypeOf<Option<number>>();
|
||||||
|
expectTypeOf(flatMappedNone).toEqualTypeOf<Option<number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle None input correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption: Option<number> = none() as Option<number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMapped = noneOption.flatMap(x => some(x * 2));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(flatMapped).toEqualTypeOf<Option<number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should transform types correctly with flatMap", () => {
|
||||||
|
// Arrange
|
||||||
|
const stringOption = some("42");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const parsed = stringOption.flatMap(str => {
|
||||||
|
const num = parseInt(str);
|
||||||
|
return isNaN(num) ? none() as Option<number> : some(num);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(parsed).toEqualTypeOf<Option<number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex type transformations", () => {
|
||||||
|
// Arrange
|
||||||
|
type ParseResult = { value: number; valid: boolean };
|
||||||
|
const inputOption = some("123");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = inputOption.flatMap((str): Option<ParseResult> => {
|
||||||
|
const num = parseInt(str);
|
||||||
|
return isNaN(num)
|
||||||
|
? none() as Option<ParseResult>
|
||||||
|
: some({ value: num, valid: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Option<ParseResult>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with chained flatMap operations", () => {
|
||||||
|
// Arrange
|
||||||
|
const initialOption = some(5);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const chained = initialOption
|
||||||
|
.flatMap(x => x > 0 ? some(x * 2) : none() as Option<number>)
|
||||||
|
.flatMap(x => x < 20 ? some(x.toString()) : none() as Option<string>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(chained).toEqualTypeOf<Option<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with correct flatMapper function signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberOption = some(42);
|
||||||
|
const stringOption = some("hello");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberOption.flatMap).toBeCallableWith((x: number) => some(x * 2));
|
||||||
|
expectTypeOf(numberOption.flatMap).toBeCallableWith((x: number) => none() as Option<number>);
|
||||||
|
expectTypeOf(stringOption.flatMap).toBeCallableWith((s: string) => some(s.length));
|
||||||
|
// @ts-expect-error - Should return Option, not raw value
|
||||||
|
numberOption.flatMap((x: number) => x * 2);
|
||||||
|
// @ts-expect-error - Wrong input type
|
||||||
|
numberOption.flatMap((s: string) => some(s.length));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("map and flatMap interaction", () => {
|
||||||
|
it("should work together in chains with correct type inference", () => {
|
||||||
|
// Arrange
|
||||||
|
const option = some(5);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option
|
||||||
|
.map(x => x * 2)
|
||||||
|
.flatMap(x => x > 5 ? some(x.toString()) : none() as Option<string>)
|
||||||
|
.map(s => s.length);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Option<number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle mixed transformations correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
type Input = { value: string };
|
||||||
|
type Output = { parsed: number; length: number };
|
||||||
|
const inputOption = some({ value: "42" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = inputOption
|
||||||
|
.map(input => input.value)
|
||||||
|
.flatMap((str): Option<number> => {
|
||||||
|
const num = parseInt(str);
|
||||||
|
return isNaN(num) ? none() as Option<number> : some(num);
|
||||||
|
})
|
||||||
|
.map((num): Output => ({ parsed: num, length: num.toString().length }));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Option<Output>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
268
src/option/spec/match.spec.ts
Normal file
268
src/option/spec/match.spec.ts
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Option, Some, None, some, none } from '../';
|
||||||
|
|
||||||
|
describe('Option.match - Runtime Tests', () => {
|
||||||
|
describe('match with Some', () => {
|
||||||
|
it('should execute onSome callback with the contained value', () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
const onSome = vi.fn((value: number) => `Value: ${value}`);
|
||||||
|
const onNone = vi.fn(() => 'No value');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = someOption.match(onSome, onNone);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(onSome).toHaveBeenCalledWith(42);
|
||||||
|
expect(onSome).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onNone).not.toHaveBeenCalled();
|
||||||
|
expect(result).toBe('Value: 42');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the result of onSome callback', () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(10);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = someOption.match(
|
||||||
|
(value) => value * 2,
|
||||||
|
() => 0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with various data types', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
{ option: some(42), expected: 'number: 42' },
|
||||||
|
{ option: some('hello'), expected: 'string: "hello"' },
|
||||||
|
{ option: some(true), expected: 'boolean: true' },
|
||||||
|
{ option: some({ id: 1 }), expected: 'object: {"id":1}' },
|
||||||
|
{ option: some([1, 2, 3]), expected: 'object: [1,2,3]' },
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ option, expected }) => {
|
||||||
|
// Act
|
||||||
|
const result = option.match(
|
||||||
|
(value) => `${typeof value}: ${JSON.stringify(value)}`,
|
||||||
|
() => 'none'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null and undefined values correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const nullOption = some(null);
|
||||||
|
const undefinedOption = some(undefined);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const nullResult = nullOption.match(
|
||||||
|
(value) => `Got null: ${value}`,
|
||||||
|
() => 'No value'
|
||||||
|
);
|
||||||
|
|
||||||
|
const undefinedResult = undefinedOption.match(
|
||||||
|
(value) => `Got undefined: ${value}`,
|
||||||
|
() => 'No value'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(nullResult).toBe('Got null: null');
|
||||||
|
expect(undefinedResult).toBe('Got undefined: undefined');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex transformations', () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some({ name: 'John', age: 30 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = someOption.match(
|
||||||
|
(person) => ({
|
||||||
|
greeting: `Hello, ${person.name}!`,
|
||||||
|
isAdult: person.age >= 18,
|
||||||
|
category: person.age < 18 ? 'minor' : person.age < 65 ? 'adult' : 'senior'
|
||||||
|
}),
|
||||||
|
() => ({ greeting: 'Hello, stranger!', isAdult: false, category: 'unknown' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual({
|
||||||
|
greeting: 'Hello, John!',
|
||||||
|
isAdult: true,
|
||||||
|
category: 'adult'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('match with None', () => {
|
||||||
|
it('should execute onNone callback', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption: Option<number> = none();
|
||||||
|
const onSome = vi.fn((value: number) => `Value: ${value}`);
|
||||||
|
const onNone = vi.fn(() => 'No value');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = noneOption.match(onSome, onNone);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(onNone).toHaveBeenCalledWith();
|
||||||
|
expect(onNone).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onSome).not.toHaveBeenCalled();
|
||||||
|
expect(result).toBe('No value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the result of onNone callback', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption = none() as Option<string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = noneOption.match(
|
||||||
|
(value) => value.toUpperCase(),
|
||||||
|
() => 'DEFAULT'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('DEFAULT');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle different return types from onNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption = none() as Option<number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringResult = noneOption.match(
|
||||||
|
(value) => value.toString(),
|
||||||
|
() => 'empty'
|
||||||
|
);
|
||||||
|
|
||||||
|
const numberResult = noneOption.match(
|
||||||
|
(value) => value,
|
||||||
|
() => -1
|
||||||
|
);
|
||||||
|
|
||||||
|
const objectResult = noneOption.match(
|
||||||
|
(value): { value: number | null } => ({ value }),
|
||||||
|
(): { value: number | null } => ({ value: null })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(stringResult).toBe('empty');
|
||||||
|
expect(numberResult).toBe(-1);
|
||||||
|
expect(objectResult).toEqual({ value: null });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('match with mixed Option types', () => {
|
||||||
|
it('should handle dynamic option types correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<string>[] = [
|
||||||
|
some('hello') as Option<string>,
|
||||||
|
none() as Option<string>,
|
||||||
|
some('world') as Option<string>
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const results = options.map(option =>
|
||||||
|
option.match(
|
||||||
|
(value) => value.toUpperCase(),
|
||||||
|
() => 'EMPTY'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results).toEqual(['HELLO', 'EMPTY', 'WORLD']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work in conditional contexts', () => {
|
||||||
|
// Arrange
|
||||||
|
const getValue = (hasValue: boolean): Option<number> =>
|
||||||
|
hasValue ? some(42) as Option<number> : none() as Option<number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const withValue = getValue(true).match(
|
||||||
|
(value) => `Found: ${value}`,
|
||||||
|
() => 'Not found'
|
||||||
|
);
|
||||||
|
|
||||||
|
const withoutValue = getValue(false).match(
|
||||||
|
(value) => `Found: ${value}`,
|
||||||
|
() => 'Not found'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(withValue).toBe('Found: 42');
|
||||||
|
expect(withoutValue).toBe('Not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('match error handling', () => {
|
||||||
|
it('should propagate errors from onSome callback', () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
const error = new Error('onSome error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(() => {
|
||||||
|
someOption.match(
|
||||||
|
() => { throw error; },
|
||||||
|
() => 'fallback'
|
||||||
|
);
|
||||||
|
}).toThrow('onSome error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should propagate errors from onNone callback', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption = none() as Option<number>;
|
||||||
|
const error = new Error('onNone error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(() => {
|
||||||
|
noneOption.match(
|
||||||
|
(value) => value.toString(),
|
||||||
|
() => { throw error; }
|
||||||
|
);
|
||||||
|
}).toThrow('onNone error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('match with async callbacks', () => {
|
||||||
|
it('should handle async onSome callback', async () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await someOption.match(
|
||||||
|
async (value) => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
|
return `Async: ${value}`;
|
||||||
|
},
|
||||||
|
async () => 'Async: No value'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('Async: 42');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle async onNone callback', async () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption: Option<number> = none();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await noneOption.match(
|
||||||
|
async (value) => `Async: ${value}`,
|
||||||
|
async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
|
return 'Async: No value';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('Async: No value');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
213
src/option/spec/match.type-spec.ts
Normal file
213
src/option/spec/match.type-spec.ts
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Option, Some, None, some, none } from "../";
|
||||||
|
|
||||||
|
describe("Option.match - Type Tests", () => {
|
||||||
|
describe("match with Some", () => {
|
||||||
|
it("should infer return type from both callback return types", () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringResult = someOption.match(
|
||||||
|
(value) => `Value: ${value}`,
|
||||||
|
() => "No value"
|
||||||
|
);
|
||||||
|
|
||||||
|
const numberResult = someOption.match(
|
||||||
|
(value) => value * 2,
|
||||||
|
() => 0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<string>();
|
||||||
|
expectTypeOf(numberResult).toEqualTypeOf<number>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should infer union type when callbacks return different types", () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const unionResult = someOption.match<string | number>(
|
||||||
|
(value) => value.toString(), // returns string
|
||||||
|
() => 0 // returns number
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(unionResult).toEqualTypeOf<string | number>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve complex return types", () => {
|
||||||
|
// Arrange
|
||||||
|
type ComplexType = { data: string[]; count: number };
|
||||||
|
const someOption = some({ id: 1, name: "test" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const complexResult = someOption.match(
|
||||||
|
(value): ComplexType => ({ data: [value.name], count: 1 }),
|
||||||
|
(): ComplexType => ({ data: [], count: 0 })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(complexResult).toEqualTypeOf<ComplexType>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle generic types correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some([1, 2, 3]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mappedResult = someOption.match(
|
||||||
|
(arr) => arr.map(x => x.toString()),
|
||||||
|
() => []
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(mappedResult).toEqualTypeOf<string[]>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("match with None", () => {
|
||||||
|
it("should infer return type from callback return types", () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption: Option<number> = none();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringResult = noneOption.match(
|
||||||
|
(value) => `Value: ${value}`,
|
||||||
|
() => "No value"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<string>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle union return types with None", () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption = none() as Option<string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const unionResult = noneOption.match<number | string>(
|
||||||
|
(value) => value.length, // returns number
|
||||||
|
() => "empty" // returns string
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(unionResult).toEqualTypeOf<number | string>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("match callback parameter types", () => {
|
||||||
|
it("should provide correct parameter type to onSome callback", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberOption = some(42);
|
||||||
|
const stringOption = some("hello");
|
||||||
|
const objectOption = some({ id: 1, name: "test" });
|
||||||
|
|
||||||
|
// Act & Assert - testing parameter types
|
||||||
|
numberOption.match(
|
||||||
|
(value) => {
|
||||||
|
expectTypeOf(value).toEqualTypeOf<number>();
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
() => 0
|
||||||
|
);
|
||||||
|
|
||||||
|
stringOption.match(
|
||||||
|
(value) => {
|
||||||
|
expectTypeOf(value).toEqualTypeOf<string>();
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
() => ""
|
||||||
|
);
|
||||||
|
|
||||||
|
objectOption.match(
|
||||||
|
(value) => {
|
||||||
|
expectTypeOf(value).toEqualTypeOf<{ id: number; name: string }>();
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
() => ({ id: 0, name: "" })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should require compatible return types from both callbacks", () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
|
||||||
|
// Act & Assert - these should compile (no @ts-expect-error needed since they should NOT raise errors)
|
||||||
|
someOption.match(
|
||||||
|
(value) => value.toString(),
|
||||||
|
() => "default"
|
||||||
|
);
|
||||||
|
|
||||||
|
someOption.match(
|
||||||
|
(value) => value,
|
||||||
|
() => 0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("match function signature validation", () => {
|
||||||
|
it("should be callable with correct callback signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
const onSome = (value: number) => value * 2;
|
||||||
|
const onNone = () => 0;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(someOption.match).toBeCallableWith(onSome, onNone);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be callable with incorrect callback signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// @ts-expect-error - wrong parameter type for onSome
|
||||||
|
someOption.match((value: string) => value, () => "");
|
||||||
|
// @ts-expect-error - onNone should not take parameters
|
||||||
|
someOption.match((x: number) => x, (value: number) => value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be callable with missing callbacks", () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// @ts-expect-error - missing onNone callback
|
||||||
|
someOption.match((x: number) => x);
|
||||||
|
// @ts-expect-error - missing both callbacks
|
||||||
|
someOption.match();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("match with complex types", () => {
|
||||||
|
it("should handle async callbacks correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const asyncResult = someOption.match(
|
||||||
|
async (value) => `Async: ${value}`,
|
||||||
|
async () => "Async: No value"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(asyncResult).toEqualTypeOf<Promise<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle callback functions as return values", () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const functionResult = someOption.match(
|
||||||
|
(value) => (x: number) => x + value,
|
||||||
|
() => (x: number) => x
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(functionResult).toEqualTypeOf<(x: number) => number>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
421
src/option/spec/namespace.spec.ts
Normal file
421
src/option/spec/namespace.spec.ts
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Option, Some, None, some, none } from '../';
|
||||||
|
|
||||||
|
describe('Option namespace - Runtime Tests', () => {
|
||||||
|
describe('Option.from', () => {
|
||||||
|
it('should return Some for non-null/undefined values', () => {
|
||||||
|
// Arrange
|
||||||
|
const numberValue = 42;
|
||||||
|
const stringValue = 'hello';
|
||||||
|
const objectValue = { id: 1 };
|
||||||
|
const arrayValue = [1, 2, 3];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberResult = Option.from(numberValue);
|
||||||
|
const stringResult = Option.from(stringValue);
|
||||||
|
const objectResult = Option.from(objectValue);
|
||||||
|
const arrayResult = Option.from(arrayValue);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(numberResult.isSome()).toBe(true);
|
||||||
|
expect(stringResult.isSome()).toBe(true);
|
||||||
|
expect(objectResult.isSome()).toBe(true);
|
||||||
|
expect(arrayResult.isSome()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return None for null', () => {
|
||||||
|
// Arrange
|
||||||
|
const value = null;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.from(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return None for undefined', () => {
|
||||||
|
// Arrange
|
||||||
|
const value = undefined;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.from(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Some for falsy values that are not null/undefined', () => {
|
||||||
|
// Arrange
|
||||||
|
const zero = 0;
|
||||||
|
const falseBool = false;
|
||||||
|
const emptyString = '';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const zeroResult = Option.from(zero);
|
||||||
|
const falseResult = Option.from(falseBool);
|
||||||
|
const emptyResult = Option.from(emptyString);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(zeroResult.isSome()).toBe(true);
|
||||||
|
expect(falseResult.isSome()).toBe(true);
|
||||||
|
expect(emptyResult.isSome()).toBe(true);
|
||||||
|
|
||||||
|
if (zeroResult.isSome()) expect(zeroResult.value).toBe(0);
|
||||||
|
if (falseResult.isSome()) expect(falseResult.value).toBe(false);
|
||||||
|
if (emptyResult.isSome()) expect(emptyResult.value).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle nullable union types', () => {
|
||||||
|
// Arrange
|
||||||
|
const withValue: string | null = 'hello';
|
||||||
|
const withoutValue: string | null = null;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const someResult = Option.from(withValue);
|
||||||
|
const noneResult = Option.from(withoutValue);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(someResult.isSome()).toBe(true);
|
||||||
|
expect(noneResult.isNone()).toBe(true);
|
||||||
|
if (someResult.isSome()) expect(someResult.value).toBe('hello');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Option.isOption', () => {
|
||||||
|
it('should return true for Some instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const someValue = some(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.isOption(someValue);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for None instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneValue = none();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.isOption(noneValue);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for non-Option values', () => {
|
||||||
|
// Arrange
|
||||||
|
const values = [42, 'hello', true, null, undefined, { isSome: true }, [1, 2, 3]];
|
||||||
|
|
||||||
|
values.forEach((value) => {
|
||||||
|
// Act
|
||||||
|
const result = Option.isOption(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for Some with new Some constructor', () => {
|
||||||
|
// Arrange
|
||||||
|
const someInstance = new Some(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.isOption(someInstance);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for None with new None constructor', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneInstance = new None();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.isOption(noneInstance);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Option.all', () => {
|
||||||
|
it('should return Some with all values when all options are Some', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [some(1), some(2), some(3)];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.all(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toEqual([1, 2, 3]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return None when any option is None', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [some(1), none(), some(3)];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.all(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Some with empty array for empty input', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.all(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toEqual([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return None when first option is None', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [none(), some(2), some(3)];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.all(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return None when last option is None', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [some(1), some(2), none()];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.all(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return None when all options are None', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [none(), none(), none()];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.all(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Option.partition', () => {
|
||||||
|
it('should partition mixed options into values and noneCount', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [some(1), none(), some(3), none()];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.partition(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.values).toEqual([1, 3]);
|
||||||
|
expect(result.noneCount).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all Some options', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<string>[] = [some('a'), some('b'), some('c')];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.partition(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.values).toEqual(['a', 'b', 'c']);
|
||||||
|
expect(result.noneCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all None options', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [none(), none(), none()];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.partition(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.values).toEqual([]);
|
||||||
|
expect(result.noneCount).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty array', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.partition(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.values).toEqual([]);
|
||||||
|
expect(result.noneCount).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Option.compact', () => {
|
||||||
|
it('should extract values from Some instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [some(1), none(), some(3)];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.compact(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual([1, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array when all None', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [none(), none()];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.compact(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all values when all Some', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<string>[] = [some('a'), some('b'), some('c')];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.compact(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(['a', 'b', 'c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for empty input', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.compact(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle falsy values in Some correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number | boolean | string>[] = [
|
||||||
|
some(0),
|
||||||
|
some(false),
|
||||||
|
some(''),
|
||||||
|
none(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.compact(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual([0, false, '']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Option.some', () => {
|
||||||
|
it('should return true when at least one option is Some', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [none(), some(2), none()];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.some(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when all options are None', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [none(), none(), none()];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.some(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when all options are Some', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [some(1), some(2), some(3)];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.some(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for empty array', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.some(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Option.every', () => {
|
||||||
|
it('should return true when all options are Some', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [some(1), some(2), some(3)];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.every(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when any option is None', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [some(1), none(), some(3)];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.every(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when all options are None', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [none(), none(), none()];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.every(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for empty array', () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.every(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
236
src/option/spec/namespace.type-spec.ts
Normal file
236
src/option/spec/namespace.type-spec.ts
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Option, Some, None, some, none, OptionPartitionResult } from "../";
|
||||||
|
|
||||||
|
describe("Option namespace - Type Tests", () => {
|
||||||
|
describe("Option.from", () => {
|
||||||
|
it("should return Option<T> for non-nullable types", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const numberResult = Option.from(42);
|
||||||
|
const stringResult = Option.from("hello");
|
||||||
|
const booleanResult = Option.from(true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberResult).toEqualTypeOf<Option<number>>();
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<Option<string>>();
|
||||||
|
expectTypeOf(booleanResult).toEqualTypeOf<Option<boolean>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return None for null type", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const result = Option.from(null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<None>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return None for undefined type", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const result = Option.from(undefined);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<None>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Option<T> for nullable union types", () => {
|
||||||
|
// Arrange
|
||||||
|
const value = "hello" as string | null;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.from(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Option<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Option<T> for undefined union types", () => {
|
||||||
|
// Arrange
|
||||||
|
const value = "hello" as string | undefined;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.from(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Option<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve falsy non-nullable types as Option", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const zeroResult = Option.from(0);
|
||||||
|
const falseResult = Option.from(false);
|
||||||
|
const emptyResult = Option.from("");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(zeroResult).toEqualTypeOf<Option<number>>();
|
||||||
|
expectTypeOf(falseResult).toEqualTypeOf<Option<boolean>>();
|
||||||
|
expectTypeOf(emptyResult).toEqualTypeOf<Option<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject async functions with a type-level error", () => {
|
||||||
|
// @ts-expect-error — async functions should not be passed to Option.from
|
||||||
|
const result = Option.from(async () => 42);
|
||||||
|
|
||||||
|
expectTypeOf(result).toBeString();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Option.isOption", () => {
|
||||||
|
it("should act as type guard narrowing to Option<unknown>", () => {
|
||||||
|
// Arrange
|
||||||
|
const value: unknown = some(42);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (Option.isOption(value)) {
|
||||||
|
expectTypeOf(value).toEqualTypeOf<Option<unknown>>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept any value", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(Option.isOption).toBeCallableWith(42);
|
||||||
|
expectTypeOf(Option.isOption).toBeCallableWith("hello");
|
||||||
|
expectTypeOf(Option.isOption).toBeCallableWith(null);
|
||||||
|
expectTypeOf(Option.isOption).toBeCallableWith(undefined);
|
||||||
|
expectTypeOf(Option.isOption).toBeCallableWith(some(42));
|
||||||
|
expectTypeOf(Option.isOption).toBeCallableWith(none());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return boolean", () => {
|
||||||
|
// Act
|
||||||
|
const result = Option.isOption(42);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<boolean>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Option.all", () => {
|
||||||
|
it("should return Option<T[]> from Option<T>[]", () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [some(1), some(2)];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.all(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Option<number[]>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle string options", () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<string>[] = [some("a"), some("b")];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.all(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Option<string[]>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept empty arrays", () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.all(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Option<number[]>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Some<T[]> when all inputs are known Some", () => {
|
||||||
|
const result = Option.all([some(1), some(2), some(3)]);
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Some<number[]>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Option.partition", () => {
|
||||||
|
it("should return OptionPartitionResult<T>", () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [some(1), none()];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.partition(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<OptionPartitionResult<number>>();
|
||||||
|
expectTypeOf(result.values).toEqualTypeOf<number[]>();
|
||||||
|
expectTypeOf(result.noneCount).toEqualTypeOf<number>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle string options", () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<string>[] = [some("a"), none()];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.partition(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<OptionPartitionResult<string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Option.compact", () => {
|
||||||
|
it("should return T[] from Option<T>[]", () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [some(1), none()];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.compact(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<number[]>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex types", () => {
|
||||||
|
// Arrange
|
||||||
|
type Item = { id: number; name: string };
|
||||||
|
const options: Option<Item>[] = [some({ id: 1, name: "a" }), none()];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.compact(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Item[]>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Option.some", () => {
|
||||||
|
it("should return boolean", () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [some(1), none()];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.some(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<boolean>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept Option<T>[] for any T", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(Option.some).toBeCallableWith([some(1), none()] as Option<number>[]);
|
||||||
|
expectTypeOf(Option.some).toBeCallableWith([some("a")] as Option<string>[]);
|
||||||
|
expectTypeOf(Option.some).toBeCallableWith([] as Option<boolean>[]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Option.every", () => {
|
||||||
|
it("should return boolean", () => {
|
||||||
|
// Arrange
|
||||||
|
const options: Option<number>[] = [some(1), some(2)];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Option.every(options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<boolean>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept Option<T>[] for any T", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(Option.every).toBeCallableWith([some(1), none()] as Option<number>[]);
|
||||||
|
expectTypeOf(Option.every).toBeCallableWith([some("a")] as Option<string>[]);
|
||||||
|
expectTypeOf(Option.every).toBeCallableWith([] as Option<boolean>[]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
219
src/option/spec/nullable.spec.ts
Normal file
219
src/option/spec/nullable.spec.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Option, Some, None, some, none } from '../';
|
||||||
|
|
||||||
|
describe('Option toNullable/toUndefined/toString - Runtime Tests', () => {
|
||||||
|
describe('toNullable() method', () => {
|
||||||
|
it('should return the value for Some', () => {
|
||||||
|
// Arrange
|
||||||
|
const option = some(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for None', () => {
|
||||||
|
// Arrange
|
||||||
|
const option = none();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle falsy values in Some correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const zeroOption = some(0);
|
||||||
|
const falseOption = some(false);
|
||||||
|
const emptyStringOption = some('');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const zeroResult = zeroOption.toNullable();
|
||||||
|
const falseResult = falseOption.toNullable();
|
||||||
|
const emptyStringResult = emptyStringOption.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(zeroResult).toBe(0);
|
||||||
|
expect(falseResult).toBe(false);
|
||||||
|
expect(emptyStringResult).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return complex objects for Some', () => {
|
||||||
|
// Arrange
|
||||||
|
const obj = { id: 1, name: 'test' };
|
||||||
|
const option = some(obj);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for None typed as Option<T>', () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<number> = none() as Option<number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toUndefined() method', () => {
|
||||||
|
it('should return the value for Some', () => {
|
||||||
|
// Arrange
|
||||||
|
const option = some(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined for None', () => {
|
||||||
|
// Arrange
|
||||||
|
const option = none();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle falsy values in Some correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const zeroOption = some(0);
|
||||||
|
const falseOption = some(false);
|
||||||
|
const emptyStringOption = some('');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const zeroResult = zeroOption.toUndefined();
|
||||||
|
const falseResult = falseOption.toUndefined();
|
||||||
|
const emptyStringResult = emptyStringOption.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(zeroResult).toBe(0);
|
||||||
|
expect(falseResult).toBe(false);
|
||||||
|
expect(emptyStringResult).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return complex objects for Some', () => {
|
||||||
|
// Arrange
|
||||||
|
const arr = [1, 2, 3];
|
||||||
|
const option = some(arr);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(arr);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined for None typed as Option<T>', () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<string> = none() as Option<string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toString() method', () => {
|
||||||
|
it('should return "Some(value)" for Some with a number', () => {
|
||||||
|
// Arrange
|
||||||
|
const option = some(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toString();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('Some(42)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "Some(value)" for Some with a string', () => {
|
||||||
|
// Arrange
|
||||||
|
const option = some('hello');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toString();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('Some(hello)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "Some(value)" for Some with a boolean', () => {
|
||||||
|
// Arrange
|
||||||
|
const trueOption = some(true);
|
||||||
|
const falseOption = some(false);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(trueOption.toString()).toBe('Some(true)');
|
||||||
|
expect(falseOption.toString()).toBe('Some(false)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "Some(value)" for Some with falsy values', () => {
|
||||||
|
// Arrange
|
||||||
|
const zeroOption = some(0);
|
||||||
|
const emptyOption = some('');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(zeroOption.toString()).toBe('Some(0)');
|
||||||
|
expect(emptyOption.toString()).toBe('Some()');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "None()" for None', () => {
|
||||||
|
// Arrange
|
||||||
|
const option = none();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toString();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('None()');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "None()" for None typed as Option<T>', () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<number> = none() as Option<number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toString();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('None()');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle object values using default coercion', () => {
|
||||||
|
// Arrange
|
||||||
|
const option = some({ id: 1 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toString();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('Some([object Object])');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null and undefined values in Some', () => {
|
||||||
|
// Arrange
|
||||||
|
const nullOption = some(null);
|
||||||
|
const undefinedOption = some(undefined);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(nullOption.toString()).toBe('Some(null)');
|
||||||
|
expect(undefinedOption.toString()).toBe('Some(undefined)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
130
src/option/spec/nullable.type-spec.ts
Normal file
130
src/option/spec/nullable.type-spec.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Option, Some, None, some, none } from "../";
|
||||||
|
|
||||||
|
describe("Option toNullable/toUndefined/toString - Type Tests", () => {
|
||||||
|
describe("toNullable() return types", () => {
|
||||||
|
it("should return T for Some<T>", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberSome = some(42);
|
||||||
|
const stringSome = some("hello");
|
||||||
|
const objectSome = some({ id: 1 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberResult = numberSome.toNullable();
|
||||||
|
const stringResult = stringSome.toNullable();
|
||||||
|
const objectResult = objectSome.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberResult).toEqualTypeOf<number>();
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<string>();
|
||||||
|
expectTypeOf(objectResult).toEqualTypeOf<{ id: number }>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null for None", () => {
|
||||||
|
// Arrange
|
||||||
|
const noneInstance = none();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = noneInstance.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<null>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return T | null for Option<T>", () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<number> = some(42) as Option<number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<number | null>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex types", () => {
|
||||||
|
// Arrange
|
||||||
|
type ComplexType = { data: string[]; count: number };
|
||||||
|
const option: Option<ComplexType> = some({ data: ["a"], count: 1 }) as Option<ComplexType>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<ComplexType | null>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toUndefined() return types", () => {
|
||||||
|
it("should return T for Some<T>", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberSome = some(42);
|
||||||
|
const stringSome = some("hello");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberResult = numberSome.toUndefined();
|
||||||
|
const stringResult = stringSome.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberResult).toEqualTypeOf<number>();
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<string>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined for None", () => {
|
||||||
|
// Arrange
|
||||||
|
const noneInstance = none();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = noneInstance.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<undefined>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return T | undefined for Option<T>", () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<string> = some("hello") as Option<string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<string | undefined>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toString() return type", () => {
|
||||||
|
it("should return string for Some", () => {
|
||||||
|
// Arrange
|
||||||
|
const someOption = some(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = someOption.toString();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<string>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return string for None", () => {
|
||||||
|
// Arrange
|
||||||
|
const noneOption = none();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = noneOption.toString();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<string>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return string for Option<T>", () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<number> = some(42) as Option<number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = option.toString();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<string>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
249
src/option/spec/option.spec.ts
Normal file
249
src/option/spec/option.spec.ts
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Option, Some, None, some, none } from '../';
|
||||||
|
|
||||||
|
describe('Option constructors and factories - Runtime Tests', () => {
|
||||||
|
describe('Some constructor', () => {
|
||||||
|
it('should create Some instance with provided value', () => {
|
||||||
|
// Arrange
|
||||||
|
const value = 42;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = new Some(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(Some);
|
||||||
|
expect(result.value).toBe(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should store different types of values correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const numberValue = 42;
|
||||||
|
const stringValue = 'hello';
|
||||||
|
const objectValue = { id: 1, name: 'test' };
|
||||||
|
const arrayValue = [1, 2, 3];
|
||||||
|
const functionValue = (x: number) => x * 2;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberSome = new Some(numberValue);
|
||||||
|
const stringSome = new Some(stringValue);
|
||||||
|
const objectSome = new Some(objectValue);
|
||||||
|
const arraySome = new Some(arrayValue);
|
||||||
|
const functionSome = new Some(functionValue);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(numberSome.value).toBe(numberValue);
|
||||||
|
expect(stringSome.value).toBe(stringValue);
|
||||||
|
expect(objectSome.value).toBe(objectValue);
|
||||||
|
expect(arraySome.value).toBe(arrayValue);
|
||||||
|
expect(functionSome.value).toBe(functionValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null and undefined values', () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const nullSome = new Some(null);
|
||||||
|
const undefinedSome = new Some(undefined);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(nullSome.value).toBeNull();
|
||||||
|
expect(undefinedSome.value).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should implement OptionInterface methods', () => {
|
||||||
|
// Arrange
|
||||||
|
const someInstance = new Some(42);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(typeof someInstance.isSome).toBe('function');
|
||||||
|
expect(typeof someInstance.isNone).toBe('function');
|
||||||
|
expect(typeof someInstance.map).toBe('function');
|
||||||
|
expect(typeof someInstance.flatMap).toBe('function');
|
||||||
|
expect(typeof someInstance.match).toBe('function');
|
||||||
|
expect(typeof someInstance.filter).toBe('function');
|
||||||
|
expect(typeof someInstance.toNullable).toBe('function');
|
||||||
|
expect(typeof someInstance.toUndefined).toBe('function');
|
||||||
|
expect(typeof someInstance.toString).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly identify as Some', () => {
|
||||||
|
// Arrange
|
||||||
|
const someInstance = new Some(42);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(someInstance.isSome()).toBe(true);
|
||||||
|
expect(someInstance.isNone()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('None constructor', () => {
|
||||||
|
it('should create None instance', () => {
|
||||||
|
// Act
|
||||||
|
const result = new None();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(None);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should implement OptionInterface methods', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneInstance = new None();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(typeof noneInstance.isSome).toBe('function');
|
||||||
|
expect(typeof noneInstance.isNone).toBe('function');
|
||||||
|
expect(typeof noneInstance.map).toBe('function');
|
||||||
|
expect(typeof noneInstance.flatMap).toBe('function');
|
||||||
|
expect(typeof noneInstance.match).toBe('function');
|
||||||
|
expect(typeof noneInstance.filter).toBe('function');
|
||||||
|
expect(typeof noneInstance.toNullable).toBe('function');
|
||||||
|
expect(typeof noneInstance.toUndefined).toBe('function');
|
||||||
|
expect(typeof noneInstance.toString).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly identify as None', () => {
|
||||||
|
// Arrange
|
||||||
|
const noneInstance = new None();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(noneInstance.isSome()).toBe(false);
|
||||||
|
expect(noneInstance.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('some() factory function', () => {
|
||||||
|
it('should create Some instance with provided value', () => {
|
||||||
|
// Arrange
|
||||||
|
const value = 42;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = some(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(Some);
|
||||||
|
expect(result.value).toBe(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with various data types', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
42,
|
||||||
|
'hello',
|
||||||
|
true,
|
||||||
|
{ id: 1, name: 'test' },
|
||||||
|
[1, 2, 3],
|
||||||
|
null,
|
||||||
|
undefined,
|
||||||
|
(x: number) => x * 2
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(testValue => {
|
||||||
|
// Act
|
||||||
|
const result = some(testValue);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(Some);
|
||||||
|
expect(result.value).toBe(testValue);
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
expect(result.isNone()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create new instances for each call', () => {
|
||||||
|
// Arrange
|
||||||
|
const value = 42;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const first = some(value);
|
||||||
|
const second = some(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(first).toBeInstanceOf(Some);
|
||||||
|
expect(second).toBeInstanceOf(Some);
|
||||||
|
expect(first).not.toBe(second); // Different instances
|
||||||
|
expect(first.value).toBe(second.value); // Same value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('none() factory function', () => {
|
||||||
|
it('should return None instance', () => {
|
||||||
|
// Act
|
||||||
|
const result = none();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(None);
|
||||||
|
expect(result.isSome()).toBe(false);
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the same singleton instance', () => {
|
||||||
|
// Act
|
||||||
|
const first = none();
|
||||||
|
const second = none();
|
||||||
|
const third = none();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(first).toBe(second);
|
||||||
|
expect(second).toBe(third);
|
||||||
|
expect(first).toBe(third);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not accept any arguments', () => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(() => none()).not.toThrow();
|
||||||
|
|
||||||
|
// TypeScript should prevent this, but testing runtime behavior
|
||||||
|
// @ts-expect-error Testing runtime behavior when called incorrectly
|
||||||
|
expect(() => none(42)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Integration tests', () => {
|
||||||
|
it('should allow creating mixed arrays of Options', () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const options: Option<number>[] = [
|
||||||
|
some(1) as Option<number>,
|
||||||
|
some(2) as Option<number>,
|
||||||
|
none() as Option<number>,
|
||||||
|
some(3) as Option<number>
|
||||||
|
];
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(options).toHaveLength(4);
|
||||||
|
expect(options[0].isSome()).toBe(true);
|
||||||
|
expect(options[1].isSome()).toBe(true);
|
||||||
|
expect(options[2].isNone()).toBe(true);
|
||||||
|
expect(options[3].isSome()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable type narrowing in runtime', () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<string> = some('hello') as Option<string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (option.isSome()) {
|
||||||
|
expect(option.value).toBe('hello');
|
||||||
|
expect(typeof option.value).toBe('string');
|
||||||
|
} else {
|
||||||
|
fail('Expected option to be Some');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle edge cases correctly', () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const zeroSome = some(0);
|
||||||
|
const falseSome = some(false);
|
||||||
|
const emptySome = some('');
|
||||||
|
const emptyArraySome = some([]);
|
||||||
|
|
||||||
|
// Assert - All should be Some instances even with "falsy" values
|
||||||
|
expect(zeroSome.isSome()).toBe(true);
|
||||||
|
expect(falseSome.isSome()).toBe(true);
|
||||||
|
expect(emptySome.isSome()).toBe(true);
|
||||||
|
expect(emptyArraySome.isSome()).toBe(true);
|
||||||
|
|
||||||
|
expect(zeroSome.value).toBe(0);
|
||||||
|
expect(falseSome.value).toBe(false);
|
||||||
|
expect(emptySome.value).toBe('');
|
||||||
|
expect(emptyArraySome.value).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
191
src/option/spec/option.type-spec.ts
Normal file
191
src/option/spec/option.type-spec.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Option, Some, None, some, none, OptionInterface } from "../";
|
||||||
|
|
||||||
|
describe("Option constructors and factories - Type Tests", () => {
|
||||||
|
describe("Some constructor", () => {
|
||||||
|
it("should create Some<T> with correct type inference", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const numberSome = new Some(42);
|
||||||
|
const stringSome = new Some("hello");
|
||||||
|
const objectSome = new Some({ id: 1, name: "test" });
|
||||||
|
const arraySome = new Some([1, 2, 3]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberSome).toEqualTypeOf<Some<number>>();
|
||||||
|
expectTypeOf(stringSome).toEqualTypeOf<Some<string>>();
|
||||||
|
expectTypeOf(objectSome).toEqualTypeOf<Some<{ id: number; name: string }>>();
|
||||||
|
expectTypeOf(arraySome).toEqualTypeOf<Some<number[]>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should implement OptionInterface with correct type parameter", () => {
|
||||||
|
// Arrange
|
||||||
|
const somePrimitive = new Some(42);
|
||||||
|
const someObject = new Some({ value: "test" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(somePrimitive).toMatchTypeOf<OptionInterface<number>>();
|
||||||
|
expectTypeOf(someObject).toMatchTypeOf<OptionInterface<{ value: string }>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be assignable to Option<T>", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberSome = new Some(42);
|
||||||
|
const stringSome = new Some("hello");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberSome).toMatchTypeOf<Option<number>>();
|
||||||
|
expectTypeOf(stringSome).toMatchTypeOf<Option<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve complex generic types", () => {
|
||||||
|
// Arrange
|
||||||
|
type ComplexType = { data: Array<{ id: number; tags: string[] }> };
|
||||||
|
const complexValue: ComplexType = { data: [{ id: 1, tags: ["a", "b"] }] };
|
||||||
|
const complexSome = new Some(complexValue);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(complexSome).toEqualTypeOf<Some<ComplexType>>();
|
||||||
|
expectTypeOf(complexSome).toMatchTypeOf<Option<ComplexType>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle nullable and undefined types correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
const nullableSome = new Some(null);
|
||||||
|
const undefinedSome = new Some(undefined);
|
||||||
|
const unionSome = new Some("hello" as string | null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(nullableSome).toEqualTypeOf<Some<null>>();
|
||||||
|
expectTypeOf(undefinedSome).toEqualTypeOf<Some<undefined>>();
|
||||||
|
expectTypeOf(unionSome).toEqualTypeOf<Some<string | null>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("None constructor", () => {
|
||||||
|
it("should create None type", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const noneInstance = new None();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(noneInstance).toEqualTypeOf<None>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should implement OptionInterface<never>", () => {
|
||||||
|
// Arrange
|
||||||
|
const noneInstance = new None();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(noneInstance).toMatchTypeOf<OptionInterface<never>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be assignable to Option<T> for any T", () => {
|
||||||
|
// Arrange
|
||||||
|
const noneInstance = new None();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(noneInstance).toMatchTypeOf<Option<number>>();
|
||||||
|
expectTypeOf(noneInstance).toMatchTypeOf<Option<string>>();
|
||||||
|
expectTypeOf(noneInstance).toMatchTypeOf<Option<any>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("some() factory function", () => {
|
||||||
|
it("should infer correct return type from value", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const numberSome = some(42);
|
||||||
|
const stringSome = some("hello");
|
||||||
|
const booleanSome = some(true);
|
||||||
|
const objectSome = some({ id: 1 });
|
||||||
|
const arraySome = some([1, 2, 3]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberSome).toEqualTypeOf<Some<number>>();
|
||||||
|
expectTypeOf(stringSome).toEqualTypeOf<Some<string>>();
|
||||||
|
expectTypeOf(booleanSome).toEqualTypeOf<Some<boolean>>();
|
||||||
|
expectTypeOf(objectSome).toEqualTypeOf<Some<{ id: number }>>();
|
||||||
|
expectTypeOf(arraySome).toEqualTypeOf<Some<number[]>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve literal types", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const literalNumber = some(42 as const);
|
||||||
|
const literalString = some("hello" as const);
|
||||||
|
const literalObject = some({ type: "user" } as const);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(literalNumber).toEqualTypeOf<Some<42>>();
|
||||||
|
expectTypeOf(literalString).toEqualTypeOf<Some<"hello">>();
|
||||||
|
expectTypeOf(literalObject).toEqualTypeOf<Some<{ readonly type: "user" }>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle function types correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
const simpleFunc = (x: number) => x * 2;
|
||||||
|
const asyncFunc = async (x: string) => x.toUpperCase();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const funcSome = some(simpleFunc);
|
||||||
|
const asyncFuncSome = some(asyncFunc);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(funcSome).toEqualTypeOf<Some<(x: number) => number>>();
|
||||||
|
expectTypeOf(asyncFuncSome).toEqualTypeOf<Some<(x: string) => Promise<string>>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with any type", () => {
|
||||||
|
// Assert - testing that some can be called with various types
|
||||||
|
expectTypeOf(some).toBeCallableWith(42);
|
||||||
|
expectTypeOf(some).toBeCallableWith("string");
|
||||||
|
expectTypeOf(some).toBeCallableWith(null);
|
||||||
|
expectTypeOf(some).toBeCallableWith(undefined);
|
||||||
|
expectTypeOf(some).toBeCallableWith({ complex: { nested: "object" } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("none() factory function", () => {
|
||||||
|
it("should return None type", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const noneResult = none();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(noneResult).toEqualTypeOf<None>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be callable with arguments", () => {
|
||||||
|
// Assert
|
||||||
|
// @ts-expect-error - none() should not accept arguments
|
||||||
|
none(42);
|
||||||
|
// @ts-expect-error - none() should not accept arguments
|
||||||
|
none("string");
|
||||||
|
// @ts-expect-error - none() should not accept arguments
|
||||||
|
none(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be assignable to any Option<T>", () => {
|
||||||
|
// Arrange
|
||||||
|
const noneResult = none();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(noneResult).toMatchTypeOf<Option<number>>();
|
||||||
|
expectTypeOf(noneResult).toMatchTypeOf<Option<string>>();
|
||||||
|
expectTypeOf(noneResult).toMatchTypeOf<Option<{ any: "object" }>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Type narrowing with constructors", () => {
|
||||||
|
it("should enable proper type narrowing with isSome/isNone", () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option<number> = some(42) as Option<number>;
|
||||||
|
|
||||||
|
// Act & Assert - TypeScript should understand type narrowing
|
||||||
|
if (option.isSome()) {
|
||||||
|
expectTypeOf(option).toEqualTypeOf<Some<number>>();
|
||||||
|
expectTypeOf(option.value).toEqualTypeOf<number>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.isNone()) {
|
||||||
|
expectTypeOf(option).toEqualTypeOf<None>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
381
src/option/spec/promise.spec.ts
Normal file
381
src/option/spec/promise.spec.ts
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Option, Some, None, some, none, OptionPromise } from '../';
|
||||||
|
|
||||||
|
describe('OptionPromise - Runtime Tests', () => {
|
||||||
|
describe('OptionPromise.from', () => {
|
||||||
|
it('should return Some when promise resolves to non-null value', async () => {
|
||||||
|
// Arrange
|
||||||
|
const fn = () => Promise.resolve(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.from(fn);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return None when promise resolves to null', async () => {
|
||||||
|
// Arrange
|
||||||
|
const fn = () => Promise.resolve(null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.from(fn);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return None when promise resolves to undefined', async () => {
|
||||||
|
// Arrange
|
||||||
|
const fn = () => Promise.resolve(undefined);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.from(fn);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Some for falsy non-null/undefined values', async () => {
|
||||||
|
// Arrange
|
||||||
|
const zeroFn = () => Promise.resolve(0);
|
||||||
|
const falseFn = () => Promise.resolve(false);
|
||||||
|
const emptyFn = () => Promise.resolve('');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const zeroResult = await OptionPromise.from(zeroFn);
|
||||||
|
const falseResult = await OptionPromise.from(falseFn);
|
||||||
|
const emptyResult = await OptionPromise.from(emptyFn);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(zeroResult.isSome()).toBe(true);
|
||||||
|
expect(falseResult.isSome()).toBe(true);
|
||||||
|
expect(emptyResult.isSome()).toBe(true);
|
||||||
|
|
||||||
|
if (zeroResult.isSome()) expect(zeroResult.value).toBe(0);
|
||||||
|
if (falseResult.isSome()) expect(falseResult.value).toBe(false);
|
||||||
|
if (emptyResult.isSome()) expect(emptyResult.value).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Some for complex objects', async () => {
|
||||||
|
// Arrange
|
||||||
|
const obj = { id: 1, name: 'test' };
|
||||||
|
const fn = () => Promise.resolve(obj);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.from(fn);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe(obj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should propagate promise rejections', async () => {
|
||||||
|
// Arrange
|
||||||
|
const error = new Error('test error');
|
||||||
|
const fn = () => Promise.reject(error);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(OptionPromise.from(fn)).rejects.toThrow('test error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OptionPromise.all', () => {
|
||||||
|
it('should return Some with all values when all resolve to Some', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(some(1) as Option<number>),
|
||||||
|
Promise.resolve(some(2) as Option<number>),
|
||||||
|
Promise.resolve(some(3) as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toEqual([1, 2, 3]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return None when any resolves to None', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(some(1) as Option<number>),
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
Promise.resolve(some(3) as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Some with empty array for empty input', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Option<number>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toEqual([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return None when all resolve to None', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OptionPromise.partition', () => {
|
||||||
|
it('should partition resolved options into values and noneCount', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(some(1) as Option<number>),
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
Promise.resolve(some(3) as Option<number>),
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.partition(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.values).toEqual([1, 3]);
|
||||||
|
expect(result.noneCount).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all Some promises', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(some('a') as Option<string>),
|
||||||
|
Promise.resolve(some('b') as Option<string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.partition(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.values).toEqual(['a', 'b']);
|
||||||
|
expect(result.noneCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all None promises', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.partition(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.values).toEqual([]);
|
||||||
|
expect(result.noneCount).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty array', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Option<number>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.partition(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.values).toEqual([]);
|
||||||
|
expect(result.noneCount).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OptionPromise.compact', () => {
|
||||||
|
it('should extract values from Some promises', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(some(1) as Option<number>),
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
Promise.resolve(some(3) as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual([1, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array when all None', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all values when all Some', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(some('a') as Option<string>),
|
||||||
|
Promise.resolve(some('b') as Option<string>),
|
||||||
|
Promise.resolve(some('c') as Option<string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(['a', 'b', 'c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for empty input', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Option<number>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OptionPromise.some', () => {
|
||||||
|
it('should return true when at least one resolves to Some', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
Promise.resolve(some(2) as Option<number>),
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.some(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when all resolve to None', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.some(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when all resolve to Some', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(some(1) as Option<number>),
|
||||||
|
Promise.resolve(some(2) as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.some(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for empty array', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Option<number>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.some(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OptionPromise.every', () => {
|
||||||
|
it('should return true when all resolve to Some', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(some(1) as Option<number>),
|
||||||
|
Promise.resolve(some(2) as Option<number>),
|
||||||
|
Promise.resolve(some(3) as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.every(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when any resolves to None', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(some(1) as Option<number>),
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
Promise.resolve(some(3) as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.every(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when all resolve to None', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.every(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for empty array', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Option<number>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await OptionPromise.every(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
151
src/option/spec/promise.type-spec.ts
Normal file
151
src/option/spec/promise.type-spec.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Option, Some, None, some, none, OptionPromise, OptionPartitionResult, OptionFromType } from "../";
|
||||||
|
|
||||||
|
describe("OptionPromise - Type Tests", () => {
|
||||||
|
describe("OptionPromise.from", () => {
|
||||||
|
it("should return Promise<Option<T>> for non-nullable resolved types", () => {
|
||||||
|
// Act
|
||||||
|
const numberResult = OptionPromise.from(() => Promise.resolve(42));
|
||||||
|
const stringResult = OptionPromise.from(() => Promise.resolve("hello"));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberResult).toEqualTypeOf<Promise<OptionFromType<number>>>();
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<Promise<OptionFromType<string>>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Promise<None> for null resolved type", () => {
|
||||||
|
// Act
|
||||||
|
const result = OptionPromise.from(() => Promise.resolve(null));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<OptionFromType<null>>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Promise<None> for undefined resolved type", () => {
|
||||||
|
// Act
|
||||||
|
const result = OptionPromise.from(() => Promise.resolve(undefined));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<OptionFromType<undefined>>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle nullable union resolved types", () => {
|
||||||
|
// Act
|
||||||
|
const result = OptionPromise.from(() => Promise.resolve("hello" as string | null));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<OptionFromType<string | null>>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should require a function that returns a promise", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(OptionPromise.from).toBeCallableWith(() => Promise.resolve(42));
|
||||||
|
expectTypeOf(OptionPromise.from).toBeCallableWith(() => Promise.resolve("test"));
|
||||||
|
// @ts-expect-error - should not accept a non-function
|
||||||
|
OptionPromise.from(42);
|
||||||
|
// @ts-expect-error - should not accept a function that doesn't return a promise
|
||||||
|
OptionPromise.from(() => 42);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("OptionPromise.all", () => {
|
||||||
|
it("should return Promise<Option<T[]>> from Promise<Option<T>>[]", () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Option<number>>[] = [
|
||||||
|
Promise.resolve(some(1) as Option<number>),
|
||||||
|
Promise.resolve(some(2) as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = OptionPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<Option<number[]>>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle string option promises", () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Option<string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = OptionPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<Option<string[]>>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("OptionPromise.partition", () => {
|
||||||
|
it("should return Promise<OptionPartitionResult<T>>", () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Option<number>>[] = [
|
||||||
|
Promise.resolve(some(1) as Option<number>),
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = OptionPromise.partition(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<OptionPartitionResult<number>>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("OptionPromise.compact", () => {
|
||||||
|
it("should return Promise<T[]> from Promise<Option<T>>[]", () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Option<number>>[] = [
|
||||||
|
Promise.resolve(some(1) as Option<number>),
|
||||||
|
Promise.resolve(none() as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = OptionPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<number[]>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex types", () => {
|
||||||
|
// Arrange
|
||||||
|
type Item = { id: number; name: string };
|
||||||
|
const promises: Promise<Option<Item>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = OptionPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<Item[]>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("OptionPromise.some", () => {
|
||||||
|
it("should return Promise<boolean>", () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Option<number>>[] = [
|
||||||
|
Promise.resolve(some(1) as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = OptionPromise.some(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<boolean>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("OptionPromise.every", () => {
|
||||||
|
it("should return Promise<boolean>", () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Option<number>>[] = [
|
||||||
|
Promise.resolve(some(1) as Option<number>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = OptionPromise.every(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<boolean>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
758
src/query/index.ts
Normal file
758
src/query/index.ts
Normal file
@@ -0,0 +1,758 @@
|
|||||||
|
import { isNullish } from '../util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T The type of the success value.
|
||||||
|
* @template E The type of the error value.
|
||||||
|
*/
|
||||||
|
export interface QueryInterface<T, E> {
|
||||||
|
/**
|
||||||
|
* Determines whether this query contains a value.
|
||||||
|
* @returns `true` if this query contains a value, `false` otherwise.
|
||||||
|
* ```ts
|
||||||
|
* okSome(42).isSome() // true
|
||||||
|
* okNone().isSome() // false
|
||||||
|
* errNone('error').isSome() // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
isSome(): this is OkSome<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether this query succeeded with no value.
|
||||||
|
* @returns `true` if this query succeeded but found no value, `false` otherwise.
|
||||||
|
* ```ts
|
||||||
|
* okSome(42).isNone() // false
|
||||||
|
* okNone().isNone() // true
|
||||||
|
* errNone('error').isNone() // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
isNone(): this is OkNone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether this query is an error.
|
||||||
|
* @returns `true` if this query is an error, `false` otherwise.
|
||||||
|
* ```ts
|
||||||
|
* okSome(42).isErr() // false
|
||||||
|
* okNone().isErr() // false
|
||||||
|
* errNone('error').isErr() // true
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
isErr(): this is ErrNone<E>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the contained value by applying a function to it.
|
||||||
|
* @param fn Function to apply to the contained value.
|
||||||
|
* @returns A new query containing the transformed value, or the original query unchanged.
|
||||||
|
* @remarks When called on a known `OkSome<T>`, returns `OkSome<U>`. On `OkNone` or `ErrNone<E>`, returns the same instance — the function is never invoked.
|
||||||
|
* ```ts
|
||||||
|
* okSome(5).map(x => x * 2) // OkSome(10)
|
||||||
|
* okNone().map(x => x * 2) // OkNone
|
||||||
|
* errNone('error').map(x => x * 2) // ErrNone('error')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
map<U>(fn: (value: T) => U): Query<U, E>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the contained error by applying a function to it.
|
||||||
|
* @param fn Function to apply to the contained error.
|
||||||
|
* @returns A new query containing the transformed error, or the original query unchanged.
|
||||||
|
* @remarks When called on a known `ErrNone<E>`, returns `ErrNone<F>`. On `OkSome<T>` or `OkNone`, returns the same instance — the function is never invoked.
|
||||||
|
* ```ts
|
||||||
|
* okSome(42).mapErr(e => e.toUpperCase()) // OkSome(42)
|
||||||
|
* okNone().mapErr(e => e.toUpperCase()) // OkNone
|
||||||
|
* errNone('error').mapErr(e => e.toUpperCase()) // ErrNone('ERROR')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
mapErr<F>(fn: (error: E) => F): Query<T, F>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a function that returns a query to the contained value and flattens the result.
|
||||||
|
* @param fn Function that takes the contained value and returns a query.
|
||||||
|
* @returns The query returned by the function, or unchanged if this query does not contain a value.
|
||||||
|
* ```ts
|
||||||
|
* okSome(5).flatMap(x => okSome(x * 2)) // OkSome(10)
|
||||||
|
* okSome(5).flatMap(x => okNone()) // OkNone
|
||||||
|
* okNone().flatMap(x => okSome(x * 2)) // OkNone
|
||||||
|
* errNone('error').flatMap(x => okSome(x * 2)) // ErrNone('error')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
flatMap<R extends Query<any, any>>(fn: (value: T) => R): R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a function that returns a query to the contained error and flattens the result.
|
||||||
|
* @param fn Function that takes the contained error and returns a query.
|
||||||
|
* @returns The query returned by the function, or unchanged if this query is not an error.
|
||||||
|
* ```ts
|
||||||
|
* okSome(42).flatMapErr(e => errNone(e.toUpperCase())) // OkSome(42)
|
||||||
|
* okNone().flatMapErr(e => errNone(e.toUpperCase())) // OkNone
|
||||||
|
* errNone('error').flatMapErr(e => errNone(e.toUpperCase())) // ErrNone('ERROR')
|
||||||
|
* errNone('error').flatMapErr(e => okSome('recovered')) // OkSome('recovered')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
flatMapErr<R extends Query<any, any>>(fn: (error: E) => R): R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes one of three functions based on whether this query contains a value, is empty, or is an error.
|
||||||
|
* @param onSome Function to execute if this query contains a value.
|
||||||
|
* @param onNone Function to execute if this query succeeded with no value.
|
||||||
|
* @param onErr Function to execute if this query is an error.
|
||||||
|
* @returns The return value of whichever function is executed. `U` is inferred as the union of all three callback return types.
|
||||||
|
* ```ts
|
||||||
|
* okSome(42).match(x => `Value: ${x}`, () => 'No value', e => `Error: ${e}`) // 'Value: 42'
|
||||||
|
* okNone().match(x => `Value: ${x}`, () => 'No value', e => `Error: ${e}`) // 'No value'
|
||||||
|
* errNone('error').match(x => `Value: ${x}`, () => 'No value', e => `Error: ${e}`) // 'Error: error'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
match<U>(onSome: (value: T) => U, onNone: () => U, onErr: (error: E) => U): U;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters this query based on a type predicate, narrowing the value type on success.
|
||||||
|
* @param predicate Type predicate function to test the contained value.
|
||||||
|
* @returns OkSome with the narrowed type if the predicate passes, OkNone otherwise. A no-op on OkNone and ErrNone.
|
||||||
|
* @remarks This overload resolves when the predicate is a type guard (`value is U`), narrowing `T` to `U`.
|
||||||
|
* Filter failure demotes OkSome to OkNone (absence), not ErrNone (error). Use `flatMap` to produce errors from conditions.
|
||||||
|
* ```ts
|
||||||
|
* okSome(42 as number | string).filter((x): x is number => typeof x === 'number') // OkSome(42): Query<number, E>
|
||||||
|
* okSome('hello' as number | string).filter((x): x is number => typeof x === 'number') // OkNone
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
filter<U extends T>(predicate: (value: T) => value is U): Query<U, E>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters this query based on a boolean predicate.
|
||||||
|
* @param predicate Function to test the contained value.
|
||||||
|
* @returns OkSome if the value satisfies the predicate, OkNone otherwise. A no-op on OkNone and ErrNone.
|
||||||
|
* @remarks This overload resolves when the predicate returns `boolean` (not a type guard). The value type `T` is preserved.
|
||||||
|
* Filter failure demotes OkSome to OkNone (absence), not ErrNone (error). Use `flatMap` to produce errors from conditions.
|
||||||
|
* ```ts
|
||||||
|
* okSome(5).filter(x => x > 3) // OkSome(5)
|
||||||
|
* okSome(2).filter(x => x > 3) // OkNone
|
||||||
|
* okNone().filter(x => x > 3) // OkNone
|
||||||
|
* errNone('error').filter(x => x > 3) // ErrNone('error')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
filter(predicate: (value: T) => boolean): Query<T, E>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the contained value or null.
|
||||||
|
* @returns The contained value if this query is OkSome, null otherwise.
|
||||||
|
* ```ts
|
||||||
|
* okSome(42).toNullable() // 42
|
||||||
|
* okNone().toNullable() // null
|
||||||
|
* errNone('error').toNullable() // null
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
toNullable(): T | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the contained value or undefined.
|
||||||
|
* @returns The contained value if this query is OkSome, undefined otherwise.
|
||||||
|
* ```ts
|
||||||
|
* okSome(42).toUndefined() // 42
|
||||||
|
* okNone().toUndefined() // undefined
|
||||||
|
* errNone('error').toUndefined() // undefined
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
toUndefined(): T | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string representation of this query.
|
||||||
|
* @returns A string describing the query type and its contents.
|
||||||
|
* ```ts
|
||||||
|
* okSome(42).toString() // 'OkSome(42)'
|
||||||
|
* okNone().toString() // 'OkNone()'
|
||||||
|
* errNone('error').toString() // 'ErrNone(error)'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a successful query containing a value of type `T`.
|
||||||
|
*
|
||||||
|
* The error type is always `never` — error-side methods (`mapErr`, `flatMapErr`) are no-ops
|
||||||
|
* that return the same `OkSome` instance unchanged.
|
||||||
|
* @remarks The contained value is accessible via the `.value` property, either directly or after narrowing with `isSome()`.
|
||||||
|
*/
|
||||||
|
export class OkSome<T> implements QueryInterface<T, any> {
|
||||||
|
constructor(readonly value: T) {}
|
||||||
|
|
||||||
|
isSome(): this is OkSome<T> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNone(): this is never {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isErr(): this is never {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
map<U>(fn: (value: T) => U): OkSome<U> {
|
||||||
|
return okSome(fn(this.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
mapErr<F>(): OkSome<T> {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
flatMap<R extends Query<any, any>>(fn: (value: T) => R): R {
|
||||||
|
return fn(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error — no-op returns self; union dispatch resolves correctly
|
||||||
|
flatMapErr(): this {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
match<U>(onSome: (value: T) => U, onNone: () => U, onErr: (error: never) => U): U {
|
||||||
|
return onSome(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
filter<U extends T>(predicate: (value: T) => value is U): OkSome<U> | OkNone;
|
||||||
|
filter(predicate: (value: T) => boolean): OkSome<T> | OkNone;
|
||||||
|
filter(predicate: (value: T) => boolean): OkSome<T> | OkNone {
|
||||||
|
return predicate(this.value) ? this : okNone();
|
||||||
|
}
|
||||||
|
|
||||||
|
toNullable(): T {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
toUndefined(): T {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `OkSome(${this.value})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a successful query that found no value.
|
||||||
|
*
|
||||||
|
* Both value and error types are `never` — all transformation methods (`map`, `mapErr`, `flatMap`,
|
||||||
|
* `flatMapErr`, `filter`) are no-ops that return the same `OkNone` instance without invoking callbacks.
|
||||||
|
* Instantiated via `okNone()`, which always returns the same frozen instance.
|
||||||
|
*/
|
||||||
|
export class OkNone implements QueryInterface<any, any> {
|
||||||
|
isSome(): this is never {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNone(): this is OkNone {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isErr(): this is never {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
map<U>(): OkNone {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapErr<F>(): OkNone {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error — no-op returns self; union dispatch resolves correctly
|
||||||
|
flatMap(): this {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error — no-op returns self; union dispatch resolves correctly
|
||||||
|
flatMapErr(): this {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
match<U>(onSome: (value: never) => U, onNone: () => U, onErr: (error: never) => U): U {
|
||||||
|
return onNone();
|
||||||
|
}
|
||||||
|
|
||||||
|
filter(): OkNone {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
toNullable(): null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
toUndefined(): undefined {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return 'OkNone()';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an error query containing an error of type `E`.
|
||||||
|
*
|
||||||
|
* The value type is always `never` — value-side methods (`map`, `flatMap`, `filter`) are no-ops
|
||||||
|
* that return the same `ErrNone` instance unchanged.
|
||||||
|
* @remarks The contained error is accessible via the `.err` property, either directly or after narrowing with `isErr()`.
|
||||||
|
*/
|
||||||
|
export class ErrNone<E> implements QueryInterface<any, E> {
|
||||||
|
constructor(readonly err: E) {}
|
||||||
|
|
||||||
|
isSome(): this is never {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNone(): this is never {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isErr(): this is ErrNone<E> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
map<U>(): ErrNone<E> {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapErr<F>(fn: (error: E) => F): ErrNone<F> {
|
||||||
|
return errNone(fn(this.err));
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error — no-op returns self; union dispatch resolves correctly
|
||||||
|
flatMap(): this {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
flatMapErr<R extends Query<any, any>>(fn: (error: E) => R): R {
|
||||||
|
return fn(this.err);
|
||||||
|
}
|
||||||
|
|
||||||
|
match<U>(onSome: (value: never) => U, onNone: () => U, onErr: (error: E) => U): U {
|
||||||
|
return onErr(this.err);
|
||||||
|
}
|
||||||
|
|
||||||
|
filter(): ErrNone<E> {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
toNullable(): null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
toUndefined(): undefined {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `ErrNone(${this.err})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a query that is either OkSome containing a value, OkNone indicating no value found, or ErrNone containing an error.
|
||||||
|
* @remarks Use `isSome()`, `isNone()`, or `isErr()` to check the query type and access the contained value via `.value` or error via `.err`.
|
||||||
|
* ```ts
|
||||||
|
* const query = okSome(42);
|
||||||
|
* if (query.isSome()) {
|
||||||
|
* console.log(query.value); // 42
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const empty = okNone();
|
||||||
|
* if (empty.isNone()) {
|
||||||
|
* console.log('No value found');
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const error = errNone('failed');
|
||||||
|
* if (error.isErr()) {
|
||||||
|
* console.log(error.err); // 'failed'
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export type Query<T, E> = OkSome<T> | OkNone | ErrNone<E>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a promise that resolves to a Query.
|
||||||
|
*/
|
||||||
|
export type QueryPromise<T, E> = Promise<Query<T, E>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a promise that resolves to OkSome.
|
||||||
|
*/
|
||||||
|
export type OkSomePromise<T> = Promise<OkSome<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a promise that resolves to OkNone.
|
||||||
|
*/
|
||||||
|
export type OkNonePromise = Promise<OkNone>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a promise that resolves to ErrNone.
|
||||||
|
*/
|
||||||
|
export type ErrNonePromise<E> = Promise<ErrNone<E>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the result of partitioning an array of queries into values, none count, and errors.
|
||||||
|
*/
|
||||||
|
export type QueryPartitionResult<T, E> = {
|
||||||
|
values: T[];
|
||||||
|
noneCount: number;
|
||||||
|
errors: E[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditional type that maps a value type to its `Query.from()` return type.
|
||||||
|
*
|
||||||
|
* When `T` is only `null | undefined`, evaluates to `OkNone`. Otherwise evaluates to `OkSome<NonNullable<T>> | OkNone`,
|
||||||
|
* where `NonNullable` strips `null` and `undefined` from the union.
|
||||||
|
*/
|
||||||
|
export type QueryFromType<T> = [NonNullable<T>] extends [never] ? OkNone : OkSome<NonNullable<T>> | OkNone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a query from a value, treating null/undefined as OkNone.
|
||||||
|
*/
|
||||||
|
const queryFromValue = <T>(value: T): QueryFromType<T> => {
|
||||||
|
return isNullish(value)
|
||||||
|
? (okNone() as QueryFromType<T>)
|
||||||
|
: (okSome(value as NonNullable<T>) as QueryFromType<T>);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a successful query containing the specified value.
|
||||||
|
* @param value The value to wrap in a query.
|
||||||
|
* @returns An OkSome containing the value.
|
||||||
|
* ```ts
|
||||||
|
* const query = okSome(42);
|
||||||
|
* if (query.isSome()) {
|
||||||
|
* console.log(query.value); // 42
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const okSome = <T>(value: T): OkSome<T> => new OkSome(value);
|
||||||
|
|
||||||
|
const OK_NONE_INSTANCE = Object.freeze(new OkNone());
|
||||||
|
/**
|
||||||
|
* Returns the singleton OkNone instance representing a successful query with no value.
|
||||||
|
* @returns The OkNone instance.
|
||||||
|
* ```ts
|
||||||
|
* okNone() // OkNone()
|
||||||
|
* okNone() === okNone() // true (same instance)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const okNone = (): OkNone => OK_NONE_INSTANCE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an error query containing the specified error.
|
||||||
|
* @param error The error to wrap in a query.
|
||||||
|
* @returns An ErrNone containing the error.
|
||||||
|
* ```ts
|
||||||
|
* const query = errNone('failed');
|
||||||
|
* if (query.isErr()) {
|
||||||
|
* console.log(query.err); // 'failed'
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const errNone = <E>(error: E): ErrNone<E> => new ErrNone(error);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines an array of queries into a single query containing an array of values.
|
||||||
|
* @param queries The array of queries to combine.
|
||||||
|
* @returns OkSome containing an array of all values if at least one query contains a value and no errors occur, OkNone if all queries are OkNone, the first ErrNone otherwise.
|
||||||
|
* @remarks Short-circuits on the first ErrNone encountered — remaining queries are not evaluated. OkNone entries are silently skipped. When all inputs are known `OkSome`, the return type narrows to `OkSome<T[]>`.
|
||||||
|
* Empty arrays return OkNone (no values to collect), unlike `Result.all`/`Option.all` which return success with an empty array.
|
||||||
|
* ```ts
|
||||||
|
* Query.all([okSome(1), okSome(2), okSome(3)]) // OkSome([1, 2, 3])
|
||||||
|
* Query.all([okSome(1), okNone(), okSome(3)]) // OkSome([1, 3])
|
||||||
|
* Query.all([okNone(), okNone()]) // OkNone
|
||||||
|
* Query.all([okSome(1), errNone('error'), okSome(3)]) // ErrNone('error')
|
||||||
|
* Query.all([]) // OkNone
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
function queryAll<T>(queries: OkSome<T>[]): OkSome<T[]>;
|
||||||
|
function queryAll<T, E>(queries: Query<T, E>[]): Query<T[], E>;
|
||||||
|
function queryAll<T, E>(queries: Query<T, E>[]): Query<T[], E> {
|
||||||
|
const values = new Array<T>(queries.length);
|
||||||
|
let len = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < queries.length; i++) {
|
||||||
|
const query = queries[i];
|
||||||
|
if (query.isSome()) {
|
||||||
|
values[len++] = query.value;
|
||||||
|
} else if (query.isErr()) {
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len === 0) return okNone();
|
||||||
|
values.length = len;
|
||||||
|
return okSome(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines an array of query promises into a single promise of a query containing an array of values.
|
||||||
|
* @param promises The array of query promises to combine.
|
||||||
|
* @returns A promise that resolves to OkSome containing an array of all values if at least one query contains a value and no errors occur, OkNone if all queries are OkNone, the first ErrNone otherwise.
|
||||||
|
* @remarks Rejects if any input promise rejects. When all inputs are known `Promise<OkSome>`, the return type narrows to `Promise<OkSome<T[]>>`.
|
||||||
|
* ```ts
|
||||||
|
* await QueryPromise.all([Promise.resolve(okSome(1)), Promise.resolve(okSome(2))]) // OkSome([1, 2])
|
||||||
|
* await QueryPromise.all([Promise.resolve(okSome(1)), Promise.resolve(okNone())]) // OkSome([1])
|
||||||
|
* await QueryPromise.all([Promise.resolve(okNone()), Promise.resolve(okNone())]) // OkNone
|
||||||
|
* await QueryPromise.all([Promise.resolve(okSome(1)), Promise.resolve(errNone('error'))]) // ErrNone('error')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async function queryPromiseAll<T>(promises: Promise<OkSome<T>>[]): Promise<OkSome<T[]>>;
|
||||||
|
async function queryPromiseAll<T, E>(promises: Promise<Query<T, E>>[]): Promise<Query<T[], E>>;
|
||||||
|
async function queryPromiseAll<T, E>(promises: Promise<Query<T, E>>[]): Promise<Query<T[], E>> {
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
return queryAll(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal Type-level guard: use `QueryPromise.from()` for async functions.
|
||||||
|
*/
|
||||||
|
function queryFrom(
|
||||||
|
fn: () => PromiseLike<any>,
|
||||||
|
errorMap?: (error: unknown) => any,
|
||||||
|
): 'ERROR: Query.from() is synchronous — use QueryPromise.from() for async functions';
|
||||||
|
/**
|
||||||
|
* Creates a query by executing a function and catching any thrown errors.
|
||||||
|
* @param fn Function to execute.
|
||||||
|
* @param errorMap Optional function to transform caught errors. When omitted, `E` defaults to `unknown` and the caught error is cast unsafely — prefer always providing `errorMap` for type safety.
|
||||||
|
* @returns OkSome containing the function's return value if successful and not null/undefined,
|
||||||
|
* OkNone if successful but returns null/undefined,
|
||||||
|
* ErrNone containing the caught (or mapped) error if the function throws.
|
||||||
|
* @remarks Only `null` and `undefined` produce OkNone. Values like `0`, `false`, and `""` produce OkSome.
|
||||||
|
* ```ts
|
||||||
|
* Query.from(() => 42) // OkSome(42)
|
||||||
|
* Query.from(() => 0) // OkSome(0)
|
||||||
|
* Query.from(() => null) // OkNone
|
||||||
|
* Query.from(() => undefined) // OkNone
|
||||||
|
* Query.from(() => { throw new Error('failed') }) // ErrNone(Error('failed')) — typed as ErrNone<unknown>
|
||||||
|
* Query.from(() => { throw new Error('failed') }, e => 'Parse failed') // ErrNone('Parse failed') — typed as ErrNone<string>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
function queryFrom<T, E = unknown>(
|
||||||
|
fn: () => T,
|
||||||
|
errorMap?: (error: unknown) => E,
|
||||||
|
): QueryFromType<T> | ErrNone<E>;
|
||||||
|
/**
|
||||||
|
* Creates a query from a nullable value.
|
||||||
|
* @param value The value to wrap.
|
||||||
|
* @returns OkSome if the value is not null/undefined, OkNone otherwise.
|
||||||
|
* @remarks Only `null` and `undefined` produce OkNone. Values like `0`, `false`, and `""` produce OkSome.
|
||||||
|
* ```ts
|
||||||
|
* Query.from(42) // OkSome(42)
|
||||||
|
* Query.from(0) // OkSome(0)
|
||||||
|
* Query.from(false) // OkSome(false)
|
||||||
|
* Query.from("") // OkSome("") — empty string is not considered falsy
|
||||||
|
* Query.from("hello" as string | null) // OkSome("hello") or OkNone depending on runtime value — typed as OkSome<string> | OkNone
|
||||||
|
* Query.from(null) // OkNone
|
||||||
|
* Query.from(undefined) // OkNone
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
function queryFrom<T>(value: T): QueryFromType<T>;
|
||||||
|
function queryFrom<T, E = unknown>(
|
||||||
|
valueOrFn: T | (() => T),
|
||||||
|
errorMap?: (error: unknown) => E,
|
||||||
|
): any {
|
||||||
|
if (typeof valueOrFn === 'function') {
|
||||||
|
try {
|
||||||
|
const result = (valueOrFn as () => T)();
|
||||||
|
return queryFromValue(result);
|
||||||
|
} catch (error) {
|
||||||
|
return errNone(errorMap ? errorMap(error) : (error as E));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return queryFromValue(valueOrFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace providing static utility methods for working with collections of queries.
|
||||||
|
*/
|
||||||
|
export const Query = {
|
||||||
|
from: queryFrom,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a value is a Query.
|
||||||
|
* @param value The value to test.
|
||||||
|
* @returns `true` if the value is a Query, `false` otherwise.
|
||||||
|
* @remarks This is a runtime `instanceof` check — it narrows to `Query<unknown, unknown>` by default. The generic parameters `T` and `E` can be specified explicitly (e.g., `Query.isQuery<number, string>(x)`) but are caller-asserted, not runtime-validated.
|
||||||
|
* ```ts
|
||||||
|
* Query.isQuery(okSome(42)) // true
|
||||||
|
* Query.isQuery(okNone()) // true
|
||||||
|
* Query.isQuery(errNone('error')) // true
|
||||||
|
* Query.isQuery(42) // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
isQuery<T, E>(value: unknown): value is Query<T, E> {
|
||||||
|
return value instanceof OkSome || value instanceof OkNone || value instanceof ErrNone;
|
||||||
|
},
|
||||||
|
|
||||||
|
all: queryAll,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Partitions an array of queries into values, none count, and errors.
|
||||||
|
* @param queries The array of queries to partition.
|
||||||
|
* @returns An object containing arrays of extracted values and errors, and the count of OkNone instances.
|
||||||
|
* ```ts
|
||||||
|
* Query.partition([okSome(1), okNone(), errNone('error')]) // { values: [1], noneCount: 1, errors: ['error'] }
|
||||||
|
* Query.partition([okSome(1), okSome(2)]) // { values: [1, 2], noneCount: 0, errors: [] }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
partition<T, E>(queries: Query<T, E>[]): QueryPartitionResult<T, E> {
|
||||||
|
const values: T[] = [];
|
||||||
|
const errors: E[] = [];
|
||||||
|
let noneCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < queries.length; i++) {
|
||||||
|
const query = queries[i];
|
||||||
|
if (query.isSome()) {
|
||||||
|
values.push(query.value);
|
||||||
|
} else if (query.isNone()) {
|
||||||
|
noneCount++;
|
||||||
|
} else {
|
||||||
|
errors.push(query.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { values, noneCount, errors };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts all values from OkSome instances in an array of queries.
|
||||||
|
* @param queries The array of queries to compact.
|
||||||
|
* @returns An array containing only the values from OkSome instances.
|
||||||
|
* ```ts
|
||||||
|
* Query.compact([okSome(1), okNone(), errNone('error'), okSome(3)]) // [1, 3]
|
||||||
|
* Query.compact([okNone(), errNone('error')]) // []
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
compact<T, E>(queries: Query<T, E>[]): T[] {
|
||||||
|
const values: T[] = [];
|
||||||
|
for (let i = 0; i < queries.length; i++) {
|
||||||
|
const q = queries[i];
|
||||||
|
if (q.isSome()) values.push(q.value);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether any query in an array succeeded.
|
||||||
|
* @param queries The array of queries to test.
|
||||||
|
* @returns `true` if at least one query is OkSome or OkNone, `false` otherwise.
|
||||||
|
* ```ts
|
||||||
|
* Query.some([okSome(1), okNone(), errNone('error')]) // true
|
||||||
|
* Query.some([okNone(), errNone('error')]) // true
|
||||||
|
* Query.some([errNone('error1'), errNone('error2')]) // false
|
||||||
|
* Query.some([]) // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
some<T, E>(queries: Query<T, E>[]): boolean {
|
||||||
|
return queries.some((q) => !q.isErr());
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether every query in an array succeeded.
|
||||||
|
* @param queries The array of queries to test.
|
||||||
|
* @returns `true` if all queries are OkSome or OkNone, `false` otherwise.
|
||||||
|
* ```ts
|
||||||
|
* Query.every([okSome(1), okNone()]) // true
|
||||||
|
* Query.every([okSome(1), errNone('error')]) // false
|
||||||
|
* Query.every([]) // true (empty array is considered valid)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
every<T, E>(queries: Query<T, E>[]): boolean {
|
||||||
|
return queries.every((q) => !q.isErr());
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace providing async utility methods for working with collections of query promises.
|
||||||
|
*/
|
||||||
|
export const QueryPromise = {
|
||||||
|
/**
|
||||||
|
* Creates a query promise by executing a function that returns a promise and catching any thrown errors.
|
||||||
|
* @param fn Function to execute that returns a promise.
|
||||||
|
* @param errorMap Optional function to transform caught errors.
|
||||||
|
* @returns A promise that resolves to OkSome containing the resolved value if successful and not null/undefined,
|
||||||
|
* OkNone if successful but resolves to null/undefined,
|
||||||
|
* ErrNone containing the caught error if the promise rejects or function throws.
|
||||||
|
* ```ts
|
||||||
|
* await QueryPromise.from(() => Promise.resolve(42)) // OkSome(42)
|
||||||
|
* await QueryPromise.from(() => Promise.resolve(null)) // OkNone
|
||||||
|
* await QueryPromise.from(() => Promise.resolve(undefined)) // OkNone
|
||||||
|
* await QueryPromise.from(() => Promise.reject(new Error('failed'))) // ErrNone(Error('failed'))
|
||||||
|
* await QueryPromise.from(() => Promise.reject(new Error('failed')), e => 'Fetch failed') // ErrNone('Fetch failed')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async from<T, E = unknown>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
errorMap?: (error: unknown) => E,
|
||||||
|
): Promise<QueryFromType<T> | ErrNone<E>> {
|
||||||
|
try {
|
||||||
|
const result = await fn();
|
||||||
|
return queryFromValue(result);
|
||||||
|
} catch (error) {
|
||||||
|
return errNone(errorMap ? errorMap(error) : (error as E));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
all: queryPromiseAll,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Partitions an array of query promises into values, none count, and errors.
|
||||||
|
* @param promises The array of query promises to partition.
|
||||||
|
* @returns A promise that resolves to an object containing arrays of extracted values and errors, and the count of OkNone instances.
|
||||||
|
* @remarks Rejects if any input promise rejects.
|
||||||
|
* ```ts
|
||||||
|
* await QueryPromise.partition([Promise.resolve(okSome(1)), Promise.resolve(okNone())]) // { values: [1], noneCount: 1, errors: [] }
|
||||||
|
* await QueryPromise.partition([Promise.resolve(okSome(1)), Promise.resolve(errNone('error'))]) // { values: [1], noneCount: 0, errors: ['error'] }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async partition<T, E>(promises: Promise<Query<T, E>>[]): Promise<QueryPartitionResult<T, E>> {
|
||||||
|
const resolved = await Promise.all(promises);
|
||||||
|
return Query.partition(resolved);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts all values from OkSome instances in an array of query promises.
|
||||||
|
* @param promises The array of query promises to compact.
|
||||||
|
* @returns A promise that resolves to an array containing only the values from OkSome instances.
|
||||||
|
* @remarks Rejects if any input promise rejects.
|
||||||
|
* ```ts
|
||||||
|
* await QueryPromise.compact([Promise.resolve(okSome(1)), Promise.resolve(okNone())]) // [1]
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async compact<T, E>(promises: Promise<Query<T, E>>[]): Promise<T[]> {
|
||||||
|
const resolved = await Promise.all(promises);
|
||||||
|
return Query.compact(resolved);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether any query promise in an array resolves to a successful query.
|
||||||
|
* @param promises The array of query promises to test.
|
||||||
|
* @returns A promise that resolves to `true` if at least one query is OkSome or OkNone, `false` otherwise.
|
||||||
|
* @remarks Rejects if any input promise rejects.
|
||||||
|
* ```ts
|
||||||
|
* await QueryPromise.some([Promise.resolve(okSome(1)), Promise.resolve(okNone())]) // true
|
||||||
|
* await QueryPromise.some([Promise.resolve(okNone()), Promise.resolve(errNone('error'))]) // true
|
||||||
|
* await QueryPromise.some([Promise.resolve(errNone('e1')), Promise.resolve(errNone('e2'))]) // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async some<T, E>(promises: Promise<Query<T, E>>[]): Promise<boolean> {
|
||||||
|
const resolved = await Promise.all(promises);
|
||||||
|
return Query.some(resolved);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether every query promise in an array resolves to a successful query.
|
||||||
|
* @param promises The array of query promises to test.
|
||||||
|
* @returns A promise that resolves to `true` if all queries are OkSome or OkNone, `false` otherwise.
|
||||||
|
* @remarks Rejects if any input promise rejects.
|
||||||
|
* ```ts
|
||||||
|
* await QueryPromise.every([Promise.resolve(okSome(1)), Promise.resolve(okNone())]) // true
|
||||||
|
* await QueryPromise.every([Promise.resolve(okSome(1)), Promise.resolve(errNone('error'))]) // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async every<T, E>(promises: Promise<Query<T, E>>[]): Promise<boolean> {
|
||||||
|
const resolved = await Promise.all(promises);
|
||||||
|
return Query.every(resolved);
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
326
src/query/spec/filter.spec.ts
Normal file
326
src/query/spec/filter.spec.ts
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Query, okSome, okNone, errNone } from '../';
|
||||||
|
|
||||||
|
describe('Query.filter - Runtime Tests', () => {
|
||||||
|
describe('filter with OkSome', () => {
|
||||||
|
it('should return OkSome when predicate returns true', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
const predicate = vi.fn((x: number) => x > 0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.filter(predicate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate).toHaveBeenCalledWith(42);
|
||||||
|
expect(predicate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkNone when predicate returns false', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
const predicate = vi.fn((x: number) => x < 0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.filter(predicate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate).toHaveBeenCalledWith(42);
|
||||||
|
expect(predicate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with various predicate conditions', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
{ value: 5, predicate: (x: number) => x > 3, expected: true },
|
||||||
|
{ value: 2, predicate: (x: number) => x > 3, expected: false },
|
||||||
|
{ value: 'hello', predicate: (x: string) => x.length > 3, expected: true },
|
||||||
|
{ value: 'hi', predicate: (x: string) => x.length > 3, expected: false },
|
||||||
|
{ value: [1, 2, 3], predicate: (x: number[]) => x.length > 0, expected: true },
|
||||||
|
{ value: [] as number[], predicate: (x: number[]) => x.length > 0, expected: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ value, predicate, expected }) => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(value);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
// @ts-expect-error Testing with various types
|
||||||
|
const result = query.filter(predicate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if (expected) {
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe(value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex object filtering', () => {
|
||||||
|
// Arrange
|
||||||
|
const users = [
|
||||||
|
{ name: 'Alice', age: 25, active: true },
|
||||||
|
{ name: 'Bob', age: 17, active: true },
|
||||||
|
{ name: 'Charlie', age: 30, active: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
users.forEach((user) => {
|
||||||
|
// Act
|
||||||
|
const activeAdult = okSome(user).filter((u) => u.active && u.age >= 18);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if (user.name === 'Alice') {
|
||||||
|
expect(activeAdult.isSome()).toBe(true);
|
||||||
|
if (activeAdult.isSome()) {
|
||||||
|
expect(activeAdult.value).toBe(user);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expect(activeAdult.isNone()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle type predicate filtering', () => {
|
||||||
|
// Arrange
|
||||||
|
const mixedValues: (string | number)[] = [42, 'hello', 100, 'world'];
|
||||||
|
|
||||||
|
mixedValues.forEach((value) => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(value);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberResult = query.filter((x): x is number => typeof x === 'number');
|
||||||
|
const stringResult = query.filter((x): x is string => typeof x === 'string');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
expect(numberResult.isSome()).toBe(true);
|
||||||
|
expect(stringResult.isNone()).toBe(true);
|
||||||
|
if (numberResult.isSome()) {
|
||||||
|
expect(numberResult.value).toBe(value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expect(numberResult.isNone()).toBe(true);
|
||||||
|
expect(stringResult.isSome()).toBe(true);
|
||||||
|
if (stringResult.isSome()) {
|
||||||
|
expect(stringResult.value).toBe(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert to OkNone on filter failure (not ErrNone)', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.filter((x) => x > 100);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
expect(result.isErr()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter with OkNone', () => {
|
||||||
|
it('should return OkNone without calling predicate', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
const predicate = vi.fn((x: number) => x > 0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.filter(predicate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate).not.toHaveBeenCalled();
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkNone for any predicate', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<string, number> = okNone() as Query<string, number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result1 = query.filter((x) => x.length > 0);
|
||||||
|
const result2 = query.filter((x) => x.length === 0);
|
||||||
|
const result3 = query.filter((x): x is string => typeof x === 'string');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result1.isNone()).toBe(true);
|
||||||
|
expect(result2.isNone()).toBe(true);
|
||||||
|
expect(result3.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter with ErrNone', () => {
|
||||||
|
it('should return ErrNone without calling predicate', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = errNone('error') as Query<number, string>;
|
||||||
|
const predicate = vi.fn((x: number) => x > 0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.filter(predicate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate).not.toHaveBeenCalled();
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve the error through filter', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = errNone('original error') as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.filter((x) => x > 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('original error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter chaining', () => {
|
||||||
|
it('should allow chaining multiple filters', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query
|
||||||
|
.filter((x) => x > 0)
|
||||||
|
.filter((x) => x < 100)
|
||||||
|
.filter((x) => x % 2 === 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should short-circuit on first failed filter', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
const predicate1 = vi.fn((x: number) => x > 0);
|
||||||
|
const predicate2 = vi.fn((x: number) => x < 10);
|
||||||
|
const predicate3 = vi.fn((x: number) => x % 2 === 0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.filter(predicate1).filter(predicate2).filter(predicate3);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate1).toHaveBeenCalledWith(42);
|
||||||
|
expect(predicate2).toHaveBeenCalledWith(42);
|
||||||
|
expect(predicate3).not.toHaveBeenCalled();
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter error handling', () => {
|
||||||
|
it('should propagate errors from predicate function', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
const error = new Error('Predicate error');
|
||||||
|
const predicate = vi.fn(() => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(() => {
|
||||||
|
query.filter(predicate);
|
||||||
|
}).toThrow('Predicate error');
|
||||||
|
|
||||||
|
expect(predicate).toHaveBeenCalledWith(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call predicate on OkNone even if it would throw', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
const predicate = vi.fn(() => {
|
||||||
|
throw new Error('Should not be called');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.filter(predicate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate).not.toHaveBeenCalled();
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call predicate on ErrNone even if it would throw', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = errNone('error') as Query<number, string>;
|
||||||
|
const predicate = vi.fn(() => {
|
||||||
|
throw new Error('Should not be called');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.filter(predicate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate).not.toHaveBeenCalled();
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter with edge cases', () => {
|
||||||
|
it('should handle falsy values correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const falsyValues = [0, false, '', NaN];
|
||||||
|
|
||||||
|
falsyValues.forEach((value) => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(value);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const truthyResult = query.filter((x) => !!x);
|
||||||
|
const falsyResult = query.filter((x) => !x);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(truthyResult.isNone()).toBe(true);
|
||||||
|
expect(falsyResult.isSome()).toBe(true);
|
||||||
|
if (falsyResult.isSome()) {
|
||||||
|
expect(falsyResult.value).toBe(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex nested structures', () => {
|
||||||
|
// Arrange
|
||||||
|
const complexObject = {
|
||||||
|
data: {
|
||||||
|
items: [
|
||||||
|
{ id: 1, name: 'Item 1', tags: ['tag1', 'tag2'] },
|
||||||
|
{ id: 2, name: 'Item 2', tags: ['tag3'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const query = okSome(complexObject);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filtered = query.filter(
|
||||||
|
(obj) => obj.data.items.length > 0 && obj.data.items.every((item) => item.tags.length > 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filtered.isSome()).toBe(true);
|
||||||
|
if (filtered.isSome()) {
|
||||||
|
expect(filtered.value).toBe(complexObject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
198
src/query/spec/filter.type-spec.ts
Normal file
198
src/query/spec/filter.type-spec.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Query, QueryInterface, OkSome, OkNone, ErrNone, okSome, okNone, errNone } from "../";
|
||||||
|
|
||||||
|
// Helper to use the interface signatures which correctly unify T and E
|
||||||
|
const qi = <T, E>(q: Query<T, E>) => q as unknown as QueryInterface<T, E>;
|
||||||
|
|
||||||
|
describe("Query.filter - Type Tests", () => {
|
||||||
|
describe("filter with type predicate", () => {
|
||||||
|
it("should narrow type with type predicate", () => {
|
||||||
|
// Arrange
|
||||||
|
const unionQuery = qi<string | number, Error>(okSome(42) as Query<string | number, Error>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberResult = unionQuery.filter((x): x is number => typeof x === "number");
|
||||||
|
const stringResult = unionQuery.filter((x): x is string => typeof x === "string");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(qi(numberResult)).toEqualTypeOf<QueryInterface<number, Error>>();
|
||||||
|
expectTypeOf(qi(stringResult)).toEqualTypeOf<QueryInterface<string, Error>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex type predicates", () => {
|
||||||
|
// Arrange
|
||||||
|
interface User {
|
||||||
|
type: "user";
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
interface Admin {
|
||||||
|
type: "admin";
|
||||||
|
name: string;
|
||||||
|
permissions: string[];
|
||||||
|
}
|
||||||
|
type Person = User | Admin;
|
||||||
|
|
||||||
|
const personQuery = qi<Person, string>(okSome({ type: "user", name: "John" }) as Query<Person, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const adminResult = personQuery.filter((p): p is Admin => p.type === "admin");
|
||||||
|
const userResult = personQuery.filter((p): p is User => p.type === "user");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(qi(adminResult)).toEqualTypeOf<QueryInterface<Admin, string>>();
|
||||||
|
expectTypeOf(qi(userResult)).toEqualTypeOf<QueryInterface<User, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle nullable type predicates", () => {
|
||||||
|
// Arrange
|
||||||
|
const nullableQuery = qi<string | null, Error>(okSome("hello") as Query<string | null, Error>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const nonNullResult = nullableQuery.filter((x): x is string => x !== null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(qi(nonNullResult)).toEqualTypeOf<QueryInterface<string, Error>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle array type predicates", () => {
|
||||||
|
// Arrange
|
||||||
|
const arrayQuery = qi<unknown[], string>(okSome([1, 2, 3]) as Query<unknown[], string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberArrayResult = arrayQuery.filter(
|
||||||
|
(arr): arr is number[] => arr.every((item) => typeof item === "number"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(qi(numberArrayResult)).toEqualTypeOf<QueryInterface<number[], string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter with boolean predicate", () => {
|
||||||
|
it("should preserve original type with boolean predicate", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberQuery = okSome(42);
|
||||||
|
const stringQuery = okSome("hello");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filteredNumber = numberQuery.filter((x) => x > 0);
|
||||||
|
const filteredString = stringQuery.filter((x) => x.length > 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(filteredNumber).toEqualTypeOf<Query<number, never>>();
|
||||||
|
expectTypeOf(filteredString).toEqualTypeOf<Query<string, never>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with complex boolean predicates", () => {
|
||||||
|
// Arrange
|
||||||
|
const objectQuery = okSome({ id: 1, name: "test", active: true });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filteredObject = objectQuery.filter((obj) => obj.active && obj.name.length > 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(filteredObject).toEqualTypeOf<Query<{ id: number; name: string; active: boolean }, never>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter with OkNone", () => {
|
||||||
|
it("should preserve Query type regardless of predicate", () => {
|
||||||
|
// Arrange
|
||||||
|
const noneQuery: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filteredWithBoolean = noneQuery.filter((x) => x > 0);
|
||||||
|
const filteredWithTypePredicate = noneQuery.filter((x): x is number => typeof x === "number");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(filteredWithBoolean).toEqualTypeOf<Query<number, string>>();
|
||||||
|
expectTypeOf(filteredWithTypePredicate).toEqualTypeOf<Query<number, string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter with ErrNone", () => {
|
||||||
|
it("should preserve Query type regardless of predicate", () => {
|
||||||
|
// Arrange
|
||||||
|
const errQuery: Query<number, string> = errNone("error") as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filteredWithBoolean = errQuery.filter((x) => x > 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(filteredWithBoolean).toEqualTypeOf<Query<number, string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter function signature validation", () => {
|
||||||
|
it("should be callable with correct predicate signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
const booleanPredicate = (x: number) => x > 0;
|
||||||
|
const typePredicate = (x: number): x is number => typeof x === "number";
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(query.filter).toBeCallableWith(booleanPredicate);
|
||||||
|
expectTypeOf(query.filter).toBeCallableWith(typePredicate);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be callable with incorrect predicate signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// @ts-expect-error - wrong parameter type for predicate
|
||||||
|
query.filter((x: string) => x.length > 0);
|
||||||
|
// @ts-expect-error - wrong return type for predicate
|
||||||
|
query.filter((x: number) => x.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be callable without predicate", () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// @ts-expect-error - missing predicate argument
|
||||||
|
query.filter();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter overload resolution", () => {
|
||||||
|
it("should resolve to type predicate overload when type predicate is provided", () => {
|
||||||
|
// Arrange
|
||||||
|
const unionQuery = qi<string | number, Error>(okSome(42) as Query<string | number, Error>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = unionQuery.filter((x): x is number => typeof x === "number");
|
||||||
|
|
||||||
|
// Assert - should be Query<number, Error>, not Query<string | number, Error>
|
||||||
|
expectTypeOf(qi(result)).toEqualTypeOf<QueryInterface<number, Error>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should resolve to boolean predicate overload when boolean predicate is provided", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberQuery = okSome(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = numberQuery.filter((x) => x > 0);
|
||||||
|
|
||||||
|
// Assert - should preserve original type
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Query<number, never>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter with generic constraints", () => {
|
||||||
|
it("should work with generic type constraints", () => {
|
||||||
|
// Arrange
|
||||||
|
interface Lengthable {
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
const lengthableQuery = qi<Lengthable, string>(okSome("hello") as Query<Lengthable, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = lengthableQuery.filter((x): x is string => typeof x === "string");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(qi(result)).toEqualTypeOf<QueryInterface<string, string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
325
src/query/spec/flatMap.spec.ts
Normal file
325
src/query/spec/flatMap.spec.ts
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Query, QueryInterface, okSome, okNone, errNone } from '../';
|
||||||
|
|
||||||
|
// Helper to use the interface signatures which correctly unify T and E
|
||||||
|
const qi = <T, E>(q: Query<T, E>): QueryInterface<T, E> => q;
|
||||||
|
|
||||||
|
describe('Query flatMap and flatMapErr - Runtime Tests', () => {
|
||||||
|
describe('flatMap method', () => {
|
||||||
|
describe('when Query is OkSome', () => {
|
||||||
|
it('should apply function and flatten the result when function returns OkSome', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: QueryInterface<number, string> = okSome(5);
|
||||||
|
const flatMapper = (x: number): Query<number, string> => okSome(x * 2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.flatMap(flatMapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe(10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkNone when function returns OkNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: QueryInterface<number, string> = okSome(5);
|
||||||
|
const flatMapper = (_x: number): Query<number, string> => okNone();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.flatMap(flatMapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return ErrNone when function returns ErrNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: QueryInterface<number, string> = okSome(5);
|
||||||
|
const flatMapper = (_x: number): Query<number, string> => errNone('failed');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.flatMap(flatMapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('failed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle conditional logic in flatMapper', () => {
|
||||||
|
// Arrange
|
||||||
|
const positiveQuery: QueryInterface<number, string> = okSome(5);
|
||||||
|
const negativeQuery: QueryInterface<number, string> = okSome(-3);
|
||||||
|
const flatMapper = (x: number): Query<number, string> =>
|
||||||
|
x > 0 ? okSome(x * 2) : okNone();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const positiveResult = positiveQuery.flatMap(flatMapper);
|
||||||
|
const negativeResult = negativeQuery.flatMap(flatMapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(positiveResult.isSome()).toBe(true);
|
||||||
|
expect(negativeResult.isNone()).toBe(true);
|
||||||
|
if (positiveResult.isSome()) {
|
||||||
|
expect(positiveResult.value).toBe(10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle type transformations', () => {
|
||||||
|
// Arrange
|
||||||
|
const stringQuery: QueryInterface<string, string> = okSome('42');
|
||||||
|
const parseNumber = (str: string): Query<number, string> => {
|
||||||
|
const num = parseInt(str);
|
||||||
|
return isNaN(num) ? okNone() : okSome(num);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const validResult = stringQuery.flatMap(parseNumber);
|
||||||
|
const invalidQuery: QueryInterface<string, string> = okSome('invalid');
|
||||||
|
const invalidResult = invalidQuery.flatMap(parseNumber);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(validResult.isSome()).toBe(true);
|
||||||
|
expect(invalidResult.isNone()).toBe(true);
|
||||||
|
if (validResult.isSome()) {
|
||||||
|
expect(validResult.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not mutate the original query', () => {
|
||||||
|
// Arrange
|
||||||
|
const original = okSome({ value: 10 });
|
||||||
|
const originalValue = original.value;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMapped = original.flatMap((obj) => okSome({ ...obj, value: obj.value * 2 }));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(original.value).toBe(originalValue);
|
||||||
|
expect(original.value.value).toBe(10);
|
||||||
|
if (flatMapped.isSome()) {
|
||||||
|
expect(flatMapped.value.value).toBe(20);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Query is OkNone', () => {
|
||||||
|
it('should return OkNone without calling the flatMapper function', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: QueryInterface<number, string> = okNone();
|
||||||
|
const flatMapperSpy = vi.fn((x: number): Query<number, string> => okSome(x * 2));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.flatMap(flatMapperSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
expect(flatMapperSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve OkNone through multiple flatMap operations', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: QueryInterface<number, string> = okNone();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const step1 = qi(query.flatMap((x): Query<number, string> => okSome(x * 2)));
|
||||||
|
const step2 = qi(step1.flatMap((x): Query<string, string> => okSome(x.toString())));
|
||||||
|
const result = step2.flatMap((s): Query<number, string> => okSome(s.length));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Query is ErrNone', () => {
|
||||||
|
it('should return ErrNone without calling the flatMapper function', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: QueryInterface<number, string> = errNone('error');
|
||||||
|
const flatMapperSpy = vi.fn((x: number): Query<number, string> => okSome(x * 2));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.flatMap(flatMapperSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('error');
|
||||||
|
}
|
||||||
|
expect(flatMapperSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve ErrNone through multiple flatMap operations', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: QueryInterface<number, string> = errNone('error');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const step1 = qi(query.flatMap((x): Query<number, string> => okSome(x * 2)));
|
||||||
|
const result = step1.flatMap((x): Query<string, string> => okSome(x.toString()));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('flatMapErr method', () => {
|
||||||
|
describe('when Query is OkSome', () => {
|
||||||
|
it('should return OkSome without calling the flatMapper function', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
|
||||||
|
// Act - OkSome.flatMapErr takes 0 arguments since E=never
|
||||||
|
const result = query.flatMapErr();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Query is OkNone', () => {
|
||||||
|
it('should return OkNone without calling the flatMapper function', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: QueryInterface<number, string> = okNone();
|
||||||
|
const flatMapperSpy = vi.fn((_e: string): Query<number, string> => errNone('new error'));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.flatMapErr(flatMapperSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
expect(flatMapperSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Query is ErrNone', () => {
|
||||||
|
it('should apply function and flatten the result when function returns ErrNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = errNone('error');
|
||||||
|
const flatMapper = (e: string) => errNone(e.toUpperCase());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.flatMapErr(flatMapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('ERROR');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkSome when function returns OkSome (recovery)', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: QueryInterface<string, string> = errNone('error');
|
||||||
|
const flatMapper = (_e: string): Query<string, string> => okSome('recovered');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.flatMapErr(flatMapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe('recovered');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkNone when function returns OkNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = errNone('error');
|
||||||
|
const flatMapper = (_e: string): Query<never, string> => okNone();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.flatMapErr(flatMapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle conditional error recovery', () => {
|
||||||
|
// Arrange
|
||||||
|
const recoverableErr: QueryInterface<string, string> = errNone('recoverable');
|
||||||
|
const fatalErr: QueryInterface<string, string> = errNone('fatal');
|
||||||
|
const flatMapper = (e: string): Query<string, string> =>
|
||||||
|
e === 'recoverable'
|
||||||
|
? okSome('recovered')
|
||||||
|
: errNone(`unrecoverable: ${e}`);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const recovered = recoverableErr.flatMapErr(flatMapper);
|
||||||
|
const notRecovered = fatalErr.flatMapErr(flatMapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(recovered.isSome()).toBe(true);
|
||||||
|
if (recovered.isSome()) expect(recovered.value).toBe('recovered');
|
||||||
|
|
||||||
|
expect(notRecovered.isErr()).toBe(true);
|
||||||
|
if (notRecovered.isErr()) expect(notRecovered.err).toBe('unrecoverable: fatal');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('flatMap and flatMapErr integration', () => {
|
||||||
|
it('should work together in complex chains', () => {
|
||||||
|
// Arrange
|
||||||
|
const input: QueryInterface<string, string> = okSome('123');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const step1 = qi(input.flatMap((str): Query<number, string> => {
|
||||||
|
const num = parseInt(str);
|
||||||
|
return isNaN(num)
|
||||||
|
? errNone('parse error')
|
||||||
|
: okSome(num);
|
||||||
|
}));
|
||||||
|
const result = step1.flatMap((num): Query<string, string> =>
|
||||||
|
num > 100
|
||||||
|
? okSome(`Large: ${num}`)
|
||||||
|
: okSome(`Small: ${num}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe('Large: 123');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should short-circuit on OkNone in chain', () => {
|
||||||
|
// Arrange
|
||||||
|
const mapSpy = vi.fn((x: string): Query<string, string> => okSome(x.toUpperCase()));
|
||||||
|
const flatMapSpy = vi.fn((x: string): Query<number, string> => okSome(x.length));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const input: QueryInterface<string, string> = okSome('test');
|
||||||
|
const step1 = qi(input.flatMap((_x): Query<string, string> => okNone()));
|
||||||
|
const step2 = qi(step1.flatMap(mapSpy));
|
||||||
|
const result = step2.flatMap(flatMapSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
expect(mapSpy).not.toHaveBeenCalled();
|
||||||
|
expect(flatMapSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should short-circuit on ErrNone in chain', () => {
|
||||||
|
// Arrange
|
||||||
|
const mapSpy = vi.fn((x: string): Query<string, string> => okSome(x.toUpperCase()));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const input: QueryInterface<string, string> = okSome('test');
|
||||||
|
const step1 = qi(input.flatMap((_x): Query<string, string> => errNone('error')));
|
||||||
|
const result = step1.flatMap(mapSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
expect(mapSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
178
src/query/spec/flatMap.type-spec.ts
Normal file
178
src/query/spec/flatMap.type-spec.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Query, QueryInterface, OkSome, OkNone, ErrNone, okSome, okNone, errNone } from "../";
|
||||||
|
|
||||||
|
// Helper to use the interface signatures which correctly unify T and E
|
||||||
|
const qi = <T, E>(q: Query<T, E>) => q as unknown as QueryInterface<T, E>;
|
||||||
|
|
||||||
|
describe("Query flatMap and flatMapErr - Type Tests", () => {
|
||||||
|
describe("flatMap directly on Query<T, E> union", () => {
|
||||||
|
it("should accept callbacks returning Query<U, E> without qi() helper", () => {
|
||||||
|
const query: Query<number, string> = okSome(42) as Query<number, string>;
|
||||||
|
const flatMapped = query.flatMap(x => x > 0 ? okSome(x * 2) : errNone("negative"));
|
||||||
|
expectTypeOf(flatMapped).toEqualTypeOf<OkSome<number> | ErrNone<string> | OkNone | ErrNone<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return exact callback type on known OkSome via R pattern", () => {
|
||||||
|
const flatMapped = okSome(42).flatMap(x => okSome(x.toString()));
|
||||||
|
expectTypeOf(flatMapped).toEqualTypeOf<OkSome<string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("flatMapErr directly on Query<T, E> union", () => {
|
||||||
|
it("should return exact callback type on known ErrNone via R pattern", () => {
|
||||||
|
const flatMapped = errNone("fail").flatMapErr(e => errNone(e.length));
|
||||||
|
expectTypeOf(flatMapped).toEqualTypeOf<ErrNone<number>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("flatMap method", () => {
|
||||||
|
it("should flatten OkSome<T> with function returning Query<U, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberQuery = okSome(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMappedSome = numberQuery.flatMap((x) => okSome(x * 2));
|
||||||
|
const flatMappedNone = numberQuery.flatMap((_x) => okNone() as Query<number, never>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(flatMappedSome).toEqualTypeOf<Query<number, never>>();
|
||||||
|
expectTypeOf(flatMappedNone).toEqualTypeOf<Query<number, never>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle OkNone input correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
const noneQuery: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMapped = noneQuery.flatMap((x) => okSome(x * 2));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(flatMapped).toEqualTypeOf<Query<number, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle ErrNone input correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
const errQuery: Query<number, string> = errNone("error") as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMapped = errQuery.flatMap((x) => okSome(x * 2));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(flatMapped).toEqualTypeOf<Query<number, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should transform types correctly with flatMap", () => {
|
||||||
|
// Arrange
|
||||||
|
const stringQuery = okSome("42");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const parsed = stringQuery.flatMap((str) => {
|
||||||
|
const num = parseInt(str);
|
||||||
|
return isNaN(num) ? (okNone() as Query<number, never>) : okSome(num);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(parsed).toEqualTypeOf<Query<number, never>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with chained flatMap operations", () => {
|
||||||
|
// Arrange
|
||||||
|
const initialQuery = okSome(5);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const chained = initialQuery
|
||||||
|
.flatMap((x) => (x > 0 ? okSome(x * 2) : (okNone() as Query<number, never>)))
|
||||||
|
.flatMap((x) => (x < 20 ? okSome(x.toString()) : (okNone() as Query<string, never>)));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(chained).toEqualTypeOf<Query<string, never>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with correct flatMapper function signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberQuery = okSome(42);
|
||||||
|
const stringQuery = okSome("hello");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberQuery.flatMap).toBeCallableWith((x: number) => okSome(x * 2));
|
||||||
|
expectTypeOf(numberQuery.flatMap).toBeCallableWith((_x: number) => okNone() as Query<number, never>);
|
||||||
|
expectTypeOf(stringQuery.flatMap).toBeCallableWith((s: string) => okSome(s.length));
|
||||||
|
// @ts-expect-error - flatMap callback must return a Query, not a plain value
|
||||||
|
numberQuery.flatMap((x: number) => x * 2);
|
||||||
|
// @ts-expect-error - wrong parameter type for flatMap callback
|
||||||
|
numberQuery.flatMap((s: string) => okSome(s.length));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("flatMapErr method", () => {
|
||||||
|
it("should flatten ErrNone<E> with function returning Query<T, F>", () => {
|
||||||
|
// Arrange
|
||||||
|
const errQuery = errNone("error");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMapped = errQuery.flatMapErr((e) => errNone(e.toUpperCase()));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(flatMapped).toEqualTypeOf<Query<never, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle OkSome input correctly (no-op)", () => {
|
||||||
|
// Arrange
|
||||||
|
const someQuery = qi<number, string>(okSome(42) as Query<number, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMapped = someQuery.flatMapErr((_e) => errNone("new error"));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(qi(flatMapped)).toEqualTypeOf<QueryInterface<number, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle OkNone input correctly (no-op)", () => {
|
||||||
|
// Arrange
|
||||||
|
const noneQuery: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMapped = noneQuery.flatMapErr((_e) => errNone(42));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(flatMapped).toEqualTypeOf<Query<number, number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle error recovery returning OkSome", () => {
|
||||||
|
// Arrange
|
||||||
|
const errQuery = qi<string, string>(errNone("error") as Query<string, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const recovered = errQuery.flatMapErr((_e) => okSome("recovered") as Query<string, never>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(qi(recovered)).toEqualTypeOf<QueryInterface<string, never>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with correct flatMapper function signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const errQuery = errNone("error");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(errQuery.flatMapErr).toBeCallableWith((e: string) => errNone(e.toUpperCase()));
|
||||||
|
expectTypeOf(errQuery.flatMapErr).toBeCallableWith((e: string) => errNone(e.length));
|
||||||
|
// @ts-expect-error - wrong parameter type for flatMapErr callback
|
||||||
|
errQuery.flatMapErr((e: number) => errNone(e.toString()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("flatMap and flatMapErr interaction", () => {
|
||||||
|
it("should work together in chains with correct type inference", () => {
|
||||||
|
// Arrange
|
||||||
|
const query = qi<number, string>(okSome(5) as Query<number, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query
|
||||||
|
.flatMap((x) => (x > 0 ? okSome(x.toString()) : (errNone("negative") as Query<string, string>)))
|
||||||
|
.flatMapErr((e) => errNone(e.length));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(qi(result)).toEqualTypeOf<QueryInterface<string, number>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
357
src/query/spec/is.spec.ts
Normal file
357
src/query/spec/is.spec.ts
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Query, okSome, okNone, errNone } from '../';
|
||||||
|
|
||||||
|
describe('Query isSome/isNone/isErr - Runtime Tests', () => {
|
||||||
|
describe('isSome() method', () => {
|
||||||
|
it('should return true for OkSome instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
okSome(42),
|
||||||
|
okSome('hello'),
|
||||||
|
okSome(true),
|
||||||
|
okSome({ id: 1 }),
|
||||||
|
okSome([1, 2, 3]),
|
||||||
|
okSome(0),
|
||||||
|
okSome(false),
|
||||||
|
okSome(''),
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach((query) => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(query.isSome()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for OkNone instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okNone();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(query.isSome()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for ErrNone instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = errNone('error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(query.isSome()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable access to value property after type narrowing', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<string, number> = okSome('hello') as Query<string, number>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (query.isSome()) {
|
||||||
|
expect(query.value).toBe('hello');
|
||||||
|
expect(typeof query.value).toBe('string');
|
||||||
|
} else {
|
||||||
|
fail('Expected query to be OkSome');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with complex objects', () => {
|
||||||
|
// Arrange
|
||||||
|
const complexObject = {
|
||||||
|
id: 1,
|
||||||
|
name: 'test',
|
||||||
|
nested: { data: [1, 2, 3] },
|
||||||
|
fn: (x: number) => x * 2,
|
||||||
|
};
|
||||||
|
const query: Query<typeof complexObject, string> = okSome(complexObject) as Query<typeof complexObject, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (query.isSome()) {
|
||||||
|
expect(query.value).toBe(complexObject);
|
||||||
|
expect(query.value.id).toBe(1);
|
||||||
|
expect(query.value.name).toBe('test');
|
||||||
|
expect(query.value.nested.data).toEqual([1, 2, 3]);
|
||||||
|
expect(query.value.fn(5)).toBe(10);
|
||||||
|
} else {
|
||||||
|
fail('Expected query to be OkSome');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with falsy values that are still OkSome', () => {
|
||||||
|
// Arrange
|
||||||
|
const falsy = [okSome(0), okSome(false), okSome('')];
|
||||||
|
|
||||||
|
falsy.forEach((query) => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(query.isSome()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isNone() method', () => {
|
||||||
|
it('should return true for OkNone instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okNone();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(query.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for OkSome instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
okSome(42),
|
||||||
|
okSome('hello'),
|
||||||
|
okSome(true),
|
||||||
|
okSome(0),
|
||||||
|
okSome(false),
|
||||||
|
okSome(''),
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach((query) => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(query.isNone()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for ErrNone instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = errNone('error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(query.isNone()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with different generic types', () => {
|
||||||
|
// Arrange
|
||||||
|
const numberNone: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
const stringNone: Query<string, number> = okNone() as Query<string, number>;
|
||||||
|
const objectNone: Query<{ id: number }, Error> = okNone() as Query<{ id: number }, Error>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(numberNone.isNone()).toBe(true);
|
||||||
|
expect(stringNone.isNone()).toBe(true);
|
||||||
|
expect(objectNone.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isErr() method', () => {
|
||||||
|
it('should return true for ErrNone instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
errNone('error'),
|
||||||
|
errNone(404),
|
||||||
|
errNone(new Error('fail')),
|
||||||
|
errNone({ code: 'ERR' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach((query) => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(query.isErr()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for OkSome instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(query.isErr()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for OkNone instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okNone();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(query.isErr()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable access to err property after type narrowing', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = errNone('failure') as Query<number, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (query.isErr()) {
|
||||||
|
expect(query.err).toBe('failure');
|
||||||
|
expect(typeof query.err).toBe('string');
|
||||||
|
} else {
|
||||||
|
fail('Expected query to be ErrNone');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Mutual exclusivity', () => {
|
||||||
|
it('should be mutually exclusive for OkSome instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [okSome(42), okSome('hello'), okSome(false), okSome(0)];
|
||||||
|
|
||||||
|
testCases.forEach((query) => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(query.isSome()).toBe(true);
|
||||||
|
expect(query.isNone()).toBe(false);
|
||||||
|
expect(query.isErr()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be mutually exclusive for OkNone instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okNone();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(query.isSome()).toBe(false);
|
||||||
|
expect(query.isNone()).toBe(true);
|
||||||
|
expect(query.isErr()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be mutually exclusive for ErrNone instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = errNone('error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(query.isSome()).toBe(false);
|
||||||
|
expect(query.isNone()).toBe(false);
|
||||||
|
expect(query.isErr()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Type narrowing in control flow', () => {
|
||||||
|
it('should enable type-safe value access in if statements', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<{ name: string; age: number }, string> = okSome({
|
||||||
|
name: 'John',
|
||||||
|
age: 30,
|
||||||
|
}) as Query<{ name: string; age: number }, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (query.isSome()) {
|
||||||
|
expect(query.value.name).toBe('John');
|
||||||
|
expect(query.value.age).toBe(30);
|
||||||
|
} else {
|
||||||
|
fail('Expected query to be OkSome');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with early returns covering all three states', () => {
|
||||||
|
// Arrange
|
||||||
|
function processQuery(query: Query<number, string>): string {
|
||||||
|
if (query.isErr()) {
|
||||||
|
return `error: ${query.err}`;
|
||||||
|
}
|
||||||
|
if (query.isNone()) {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
return `value: ${query.value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const someResult = processQuery(okSome(42) as Query<number, string>);
|
||||||
|
const noneResult = processQuery(okNone() as Query<number, string>);
|
||||||
|
const errResult = processQuery(errNone('fail') as Query<number, string>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(someResult).toBe('value: 42');
|
||||||
|
expect(noneResult).toBe('none');
|
||||||
|
expect(errResult).toBe('error: fail');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with negated conditions', () => {
|
||||||
|
// Arrange
|
||||||
|
const someQuery: Query<string, number> = okSome('hello') as Query<string, number>;
|
||||||
|
const noneQuery: Query<string, number> = okNone() as Query<string, number>;
|
||||||
|
const errQuery: Query<string, number> = errNone(42) as Query<string, number>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (!someQuery.isNone() && !someQuery.isErr()) {
|
||||||
|
expect(someQuery.value).toBe('hello');
|
||||||
|
} else {
|
||||||
|
fail('Expected query to be OkSome');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noneQuery.isSome() && !noneQuery.isErr()) {
|
||||||
|
expect(noneQuery.isNone()).toBe(true);
|
||||||
|
} else {
|
||||||
|
fail('Expected query to be OkNone');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!errQuery.isSome() && !errQuery.isNone()) {
|
||||||
|
expect(errQuery.err).toBe(42);
|
||||||
|
} else {
|
||||||
|
fail('Expected query to be ErrNone');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Performance and consistency', () => {
|
||||||
|
it('should return consistent results across multiple calls', () => {
|
||||||
|
// Arrange
|
||||||
|
const someQuery = okSome(42);
|
||||||
|
const noneQuery = okNone();
|
||||||
|
const errQuery = errNone('error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(someQuery.isSome()).toBe(someQuery.isSome());
|
||||||
|
expect(someQuery.isNone()).toBe(someQuery.isNone());
|
||||||
|
expect(someQuery.isErr()).toBe(someQuery.isErr());
|
||||||
|
expect(noneQuery.isSome()).toBe(noneQuery.isSome());
|
||||||
|
expect(noneQuery.isNone()).toBe(noneQuery.isNone());
|
||||||
|
expect(noneQuery.isErr()).toBe(noneQuery.isErr());
|
||||||
|
expect(errQuery.isSome()).toBe(errQuery.isSome());
|
||||||
|
expect(errQuery.isNone()).toBe(errQuery.isNone());
|
||||||
|
expect(errQuery.isErr()).toBe(errQuery.isErr());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be callable without side effects', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome({ counter: 0 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const before = query.value.counter;
|
||||||
|
query.isSome();
|
||||||
|
query.isNone();
|
||||||
|
query.isErr();
|
||||||
|
query.isSome();
|
||||||
|
const after = query.value.counter;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(before).toBe(after);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge cases', () => {
|
||||||
|
it('should handle deeply nested objects', () => {
|
||||||
|
// Arrange
|
||||||
|
const deepObject = {
|
||||||
|
level1: {
|
||||||
|
level2: {
|
||||||
|
level3: {
|
||||||
|
value: 'deep',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const query: Query<typeof deepObject, string> = okSome(deepObject) as Query<typeof deepObject, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (query.isSome()) {
|
||||||
|
expect(query.value.level1.level2.level3.value).toBe('deep');
|
||||||
|
} else {
|
||||||
|
fail('Expected query to be OkSome');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle arrays with mixed types', () => {
|
||||||
|
// Arrange
|
||||||
|
const mixedArray = [1, 'string', true, null, { id: 1 }];
|
||||||
|
const query: Query<typeof mixedArray, string> = okSome(mixedArray) as Query<typeof mixedArray, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (query.isSome()) {
|
||||||
|
expect(query.value).toHaveLength(5);
|
||||||
|
expect(query.value[0]).toBe(1);
|
||||||
|
expect(query.value[1]).toBe('string');
|
||||||
|
expect(query.value[2]).toBe(true);
|
||||||
|
expect(query.value[3]).toBeNull();
|
||||||
|
expect(query.value[4]).toEqual({ id: 1 });
|
||||||
|
} else {
|
||||||
|
fail('Expected query to be OkSome');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
197
src/query/spec/is.type-spec.ts
Normal file
197
src/query/spec/is.type-spec.ts
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Query, OkSome, OkNone, ErrNone, okSome, okNone, errNone } from "../";
|
||||||
|
|
||||||
|
describe("Query isSome/isNone/isErr - Type Tests", () => {
|
||||||
|
describe("isSome() type guard", () => {
|
||||||
|
it("should act as type guard narrowing Query<T, E> to OkSome<T>", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okSome(42) as Query<number, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (query.isSome()) {
|
||||||
|
expectTypeOf(query).toEqualTypeOf<OkSome<number>>();
|
||||||
|
expectTypeOf(query.value).toEqualTypeOf<number>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return type predicate 'this is OkSome<T>'", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberQuery: Query<number, string> = okSome(42) as Query<number, string>;
|
||||||
|
const stringQuery: Query<string, number> = okSome("hello") as Query<string, number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberResult = numberQuery.isSome();
|
||||||
|
const stringResult = stringQuery.isSome();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberResult).toEqualTypeOf<boolean>();
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<boolean>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should enable access to value property after type narrowing", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<{ name: string; age: number }, string> = okSome({ name: "John", age: 30 }) as Query<{ name: string; age: number }, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (query.isSome()) {
|
||||||
|
expectTypeOf(query.value).toEqualTypeOf<{ name: string; age: number }>();
|
||||||
|
expectTypeOf(query.value.name).toEqualTypeOf<string>();
|
||||||
|
expectTypeOf(query.value.age).toEqualTypeOf<number>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with union types", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<string | number, Error> = okSome("hello") as Query<string | number, Error>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (query.isSome()) {
|
||||||
|
expectTypeOf(query).toEqualTypeOf<OkSome<string | number>>();
|
||||||
|
expectTypeOf(query.value).toEqualTypeOf<string | number>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isNone() type guard", () => {
|
||||||
|
it("should act as type guard narrowing Query<T, E> to OkNone", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (query.isNone()) {
|
||||||
|
expectTypeOf(query).toEqualTypeOf<OkNone>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return type predicate 'this is OkNone'", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.isNone();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<boolean>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should narrow to OkNone regardless of original generic types", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberQuery: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
const complexQuery: Query<{ data: Array<string> }, Error> = okNone() as Query<{ data: Array<string> }, Error>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (numberQuery.isNone()) {
|
||||||
|
expectTypeOf(numberQuery).toEqualTypeOf<OkNone>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (complexQuery.isNone()) {
|
||||||
|
expectTypeOf(complexQuery).toEqualTypeOf<OkNone>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isErr() type guard", () => {
|
||||||
|
it("should act as type guard narrowing Query<T, E> to ErrNone<E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = errNone("error") as Query<number, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (query.isErr()) {
|
||||||
|
expectTypeOf(query).toEqualTypeOf<ErrNone<string>>();
|
||||||
|
expectTypeOf(query.err).toEqualTypeOf<string>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return type predicate 'this is ErrNone<E>'", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = errNone("error") as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.isErr();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<boolean>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should enable access to err property after type narrowing", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, { code: string; message: string }> = errNone({ code: "ERR", message: "fail" }) as Query<number, { code: string; message: string }>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (query.isErr()) {
|
||||||
|
expectTypeOf(query.err).toEqualTypeOf<{ code: string; message: string }>();
|
||||||
|
expectTypeOf(query.err.code).toEqualTypeOf<string>();
|
||||||
|
expectTypeOf(query.err.message).toEqualTypeOf<string>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Type narrowing patterns", () => {
|
||||||
|
it("should enable exhaustive type checking with if-else", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<string, number> = okSome("test") as Query<string, number>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (query.isSome()) {
|
||||||
|
expectTypeOf(query).toEqualTypeOf<OkSome<string>>();
|
||||||
|
expectTypeOf(query.value).toEqualTypeOf<string>();
|
||||||
|
} else if (query.isNone()) {
|
||||||
|
expectTypeOf(query).toEqualTypeOf<OkNone>();
|
||||||
|
} else {
|
||||||
|
expectTypeOf(query).toEqualTypeOf<ErrNone<number>>();
|
||||||
|
expectTypeOf(query.err).toEqualTypeOf<number>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with early returns", () => {
|
||||||
|
// Arrange
|
||||||
|
function processQuery(query: Query<number, string>): number {
|
||||||
|
if (query.isErr()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (query.isNone()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return query.value * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testQuery: Query<number, string> = okSome(5) as Query<number, string>;
|
||||||
|
const result = processQuery(testQuery);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<number>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with negated conditions", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<boolean, string> = okSome(true) as Query<boolean, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (!query.isNone() && !query.isErr()) {
|
||||||
|
expectTypeOf(query).toEqualTypeOf<OkSome<boolean>>();
|
||||||
|
expectTypeOf(query.value).toEqualTypeOf<boolean>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!query.isSome() && !query.isErr()) {
|
||||||
|
expectTypeOf(query).toEqualTypeOf<OkNone>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!query.isSome() && !query.isNone()) {
|
||||||
|
expectTypeOf(query).toEqualTypeOf<ErrNone<string>>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Method chaining with type narrowing", () => {
|
||||||
|
it("should preserve type information in method chains", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okSome(42) as Query<number, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (query.isSome()) {
|
||||||
|
const mapped = query.map((x) => x.toString());
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<OkSome<string>>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
266
src/query/spec/map.spec.ts
Normal file
266
src/query/spec/map.spec.ts
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Query, okSome, okNone, errNone } from '../';
|
||||||
|
|
||||||
|
describe('Query map and mapErr - Runtime Tests', () => {
|
||||||
|
describe('map method', () => {
|
||||||
|
describe('when Query is OkSome', () => {
|
||||||
|
it('should transform the value using the provided function', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(5);
|
||||||
|
const mapper = (x: number) => x * 2;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.map(mapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe(10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle different type transformations', () => {
|
||||||
|
// Arrange
|
||||||
|
const numberQuery = okSome(42);
|
||||||
|
const stringQuery = okSome('hello');
|
||||||
|
const booleanQuery = okSome(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberToString = numberQuery.map((x) => x.toString());
|
||||||
|
const stringToNumber = stringQuery.map((s) => s.length);
|
||||||
|
const booleanToString = booleanQuery.map((b) => (b ? 'yes' : 'no'));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(numberToString.isSome()).toBe(true);
|
||||||
|
expect(stringToNumber.isSome()).toBe(true);
|
||||||
|
expect(booleanToString.isSome()).toBe(true);
|
||||||
|
|
||||||
|
if (numberToString.isSome()) expect(numberToString.value).toBe('42');
|
||||||
|
if (stringToNumber.isSome()) expect(stringToNumber.value).toBe(5);
|
||||||
|
if (booleanToString.isSome()) expect(booleanToString.value).toBe('yes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex object transformations', () => {
|
||||||
|
// Arrange
|
||||||
|
const user = { id: 1, name: 'John', age: 30 };
|
||||||
|
const userQuery = okSome(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const userSummary = userQuery.map((u) => ({
|
||||||
|
displayName: `${u.name} (${u.age})`,
|
||||||
|
isAdult: u.age >= 18,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(userSummary.isSome()).toBe(true);
|
||||||
|
if (userSummary.isSome()) {
|
||||||
|
expect(userSummary.value).toEqual({
|
||||||
|
displayName: 'John (30)',
|
||||||
|
isAdult: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not mutate the original query', () => {
|
||||||
|
// Arrange
|
||||||
|
const original = okSome({ count: 5 });
|
||||||
|
const originalValue = original.value;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mapped = original.map((obj) => ({ ...obj, count: obj.count * 2 }));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(original.value).toBe(originalValue);
|
||||||
|
expect(original.value.count).toBe(5);
|
||||||
|
if (mapped.isSome()) {
|
||||||
|
expect(mapped.value.count).toBe(10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Query is OkNone', () => {
|
||||||
|
it('should return OkNone without calling the mapper function', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
const mapperSpy = vi.fn((x: number) => x * 2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.map(mapperSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
expect(mapperSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve OkNone through multiple map operations', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query
|
||||||
|
.map((x) => x * 2)
|
||||||
|
.map((x) => x.toString())
|
||||||
|
.map((s) => s.length);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Query is ErrNone', () => {
|
||||||
|
it('should return ErrNone without calling the mapper function', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = errNone('error') as Query<number, string>;
|
||||||
|
const mapperSpy = vi.fn((x: number) => x * 2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.map(mapperSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('error');
|
||||||
|
}
|
||||||
|
expect(mapperSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve ErrNone through multiple map operations', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = errNone('error') as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query
|
||||||
|
.map((x) => x * 2)
|
||||||
|
.map((x) => x.toString())
|
||||||
|
.map((s) => s.length);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mapErr method', () => {
|
||||||
|
describe('when Query is OkSome', () => {
|
||||||
|
it('should return OkSome unchanged', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
|
||||||
|
// Act - OkSome.mapErr takes 0 arguments since E=never
|
||||||
|
const result = query.mapErr();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Query is OkNone', () => {
|
||||||
|
it('should return OkNone without calling the mapper function', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
const mapperSpy = vi.fn((e: string) => e.toUpperCase());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.mapErr(mapperSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
expect(mapperSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Query is ErrNone', () => {
|
||||||
|
it('should transform the error using the provided function', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = errNone('error');
|
||||||
|
const mapper = (e: string) => e.toUpperCase();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.mapErr(mapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('ERROR');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle different error type transformations', () => {
|
||||||
|
// Arrange
|
||||||
|
const stringErr = errNone('error');
|
||||||
|
const numberErr = errNone(404);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringToNumber = stringErr.mapErr((e) => e.length);
|
||||||
|
const numberToString = numberErr.mapErr((e) => `Error ${e}`);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(stringToNumber.isErr()).toBe(true);
|
||||||
|
expect(numberToString.isErr()).toBe(true);
|
||||||
|
|
||||||
|
if (stringToNumber.isErr()) expect(stringToNumber.err).toBe(5);
|
||||||
|
if (numberToString.isErr()) expect(numberToString.err).toBe('Error 404');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not mutate the original error', () => {
|
||||||
|
// Arrange
|
||||||
|
const original = errNone({ code: 'ERR', message: 'fail' });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mapped = original.mapErr((e) => ({ ...e, message: e.message.toUpperCase() }));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(original.err.message).toBe('fail');
|
||||||
|
if (mapped.isErr()) {
|
||||||
|
expect(mapped.err.message).toBe('FAIL');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('map and mapErr integration', () => {
|
||||||
|
it('should allow chaining map and mapErr', () => {
|
||||||
|
// Arrange
|
||||||
|
const someQuery: Query<number, string> = okSome(5) as Query<number, string>;
|
||||||
|
const errQuery: Query<number, string> = errNone('error') as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const someResult = someQuery.map((x) => x * 2).mapErr((e) => e.toUpperCase());
|
||||||
|
const errResult = errQuery.map((x) => x * 2).mapErr((e) => e.toUpperCase());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(someResult.isSome()).toBe(true);
|
||||||
|
if (someResult.isSome()) expect(someResult.value).toBe(10);
|
||||||
|
|
||||||
|
expect(errResult.isErr()).toBe(true);
|
||||||
|
if (errResult.isErr()) expect(errResult.err).toBe('ERROR');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle edge cases with falsy values', () => {
|
||||||
|
// Arrange
|
||||||
|
const zeroQuery = okSome(0);
|
||||||
|
const emptyStringQuery = okSome('');
|
||||||
|
const falseQuery = okSome(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const zeroResult = zeroQuery.map((x) => x + 1);
|
||||||
|
const emptyResult = emptyStringQuery.map((s) => s.length);
|
||||||
|
const falseResult = falseQuery.map((b) => !b);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(zeroResult.isSome()).toBe(true);
|
||||||
|
expect(emptyResult.isSome()).toBe(true);
|
||||||
|
expect(falseResult.isSome()).toBe(true);
|
||||||
|
|
||||||
|
if (zeroResult.isSome()) expect(zeroResult.value).toBe(1);
|
||||||
|
if (emptyResult.isSome()) expect(emptyResult.value).toBe(0);
|
||||||
|
if (falseResult.isSome()) expect(falseResult.value).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
210
src/query/spec/map.type-spec.ts
Normal file
210
src/query/spec/map.type-spec.ts
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Query, QueryInterface, OkSome, OkNone, ErrNone, okSome, okNone, errNone } from "../";
|
||||||
|
|
||||||
|
// Helper to use the interface signatures which correctly unify T and E
|
||||||
|
const qi = <T, E>(q: Query<T, E>) => q as unknown as QueryInterface<T, E>;
|
||||||
|
|
||||||
|
describe("Query map and mapErr - Type Tests", () => {
|
||||||
|
describe("map method", () => {
|
||||||
|
it("should transform OkSome<T> to Query<U, E> with correct type inference", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberQuery = okSome(42);
|
||||||
|
const stringQuery = okSome("hello");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const doubledNumber = numberQuery.map((x) => x * 2);
|
||||||
|
const uppercaseString = stringQuery.map((s) => s.toUpperCase());
|
||||||
|
const numberToString = numberQuery.map((x) => x.toString());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(doubledNumber).toEqualTypeOf<OkSome<number>>();
|
||||||
|
expectTypeOf(uppercaseString).toEqualTypeOf<OkSome<string>>();
|
||||||
|
expectTypeOf(numberToString).toEqualTypeOf<OkSome<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Query<U, E> when called on Query<T, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okSome(42) as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mapped = query.map((x) => x.toString());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<Query<string, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex type transformations", () => {
|
||||||
|
// Arrange
|
||||||
|
type User = { id: number; name: string };
|
||||||
|
type UserDto = { userId: number; displayName: string };
|
||||||
|
const userQuery = okSome({ id: 1, name: "John" } as User);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const userDtoQuery = userQuery.map(
|
||||||
|
(user): UserDto => ({
|
||||||
|
userId: user.id,
|
||||||
|
displayName: user.name.toUpperCase(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(userDtoQuery).toEqualTypeOf<OkSome<UserDto>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with correct mapper function signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberQuery = okSome(42);
|
||||||
|
const stringQuery = okSome("hello");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberQuery.map).toBeCallableWith((x: number) => x * 2);
|
||||||
|
expectTypeOf(numberQuery.map).toBeCallableWith((x: number) => x.toString());
|
||||||
|
expectTypeOf(stringQuery.map).toBeCallableWith((s: string) => s.length);
|
||||||
|
// @ts-expect-error - wrong parameter type for map callback
|
||||||
|
numberQuery.map((s: string) => s.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("narrow return types on concrete variants", () => {
|
||||||
|
it("should return OkSome<U> when map is called on a known OkSome", () => {
|
||||||
|
const mapped = okSome(42).map(x => x.toString());
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<OkSome<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return OkSome<T> when mapErr is called on a known OkSome", () => {
|
||||||
|
const mapped = okSome(42).mapErr(e => String(e));
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<OkSome<number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return OkSome<T> when flatMapErr is called on a known OkSome", () => {
|
||||||
|
const mapped = okSome(42).flatMapErr(e => errNone(String(e)));
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<OkSome<number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return OkNone when map is called on a known OkNone", () => {
|
||||||
|
const mapped = okNone().map(x => String(x));
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<OkNone>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return OkNone when mapErr is called on a known OkNone", () => {
|
||||||
|
const mapped = okNone().mapErr(e => String(e));
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<OkNone>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return OkNone when flatMap is called on a known OkNone", () => {
|
||||||
|
const mapped = okNone().flatMap(x => okSome(String(x)));
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<OkNone>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return OkNone when flatMapErr is called on a known OkNone", () => {
|
||||||
|
const mapped = okNone().flatMapErr(e => errNone(String(e)));
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<OkNone>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return OkNone when filter is called on a known OkNone", () => {
|
||||||
|
const filtered = okNone().filter();
|
||||||
|
expectTypeOf(filtered).toEqualTypeOf<OkNone>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return ErrNone<E> when map is called on a known ErrNone", () => {
|
||||||
|
const mapped = errNone("fail").map(x => String(x));
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<ErrNone<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return ErrNone<F> when mapErr is called on a known ErrNone", () => {
|
||||||
|
const mapped = errNone("fail").mapErr(e => e.length);
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<ErrNone<number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return ErrNone<E> when flatMap is called on a known ErrNone", () => {
|
||||||
|
const mapped = errNone("fail").flatMap(x => okSome(String(x)));
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<ErrNone<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return ErrNone<E> when filter is called on a known ErrNone", () => {
|
||||||
|
const filtered = errNone("fail").filter();
|
||||||
|
expectTypeOf(filtered).toEqualTypeOf<ErrNone<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return OkSome<U> when flatMap callback returns OkSome", () => {
|
||||||
|
const result = okSome(5).flatMap(x => okSome(x.toString()));
|
||||||
|
expectTypeOf(result).toEqualTypeOf<OkSome<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Query<U, never> when flatMap callback returns Query", () => {
|
||||||
|
const result = okSome(5).flatMap(x => (x > 0 ? okSome(x) : okNone()) as Query<number, never>);
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Query<number, never>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return ErrNone<F> when flatMapErr callback returns ErrNone", () => {
|
||||||
|
const result = errNone("fail").flatMapErr(e => errNone(e.length));
|
||||||
|
expectTypeOf(result).toEqualTypeOf<ErrNone<number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Query<never, F> when flatMapErr callback returns Query", () => {
|
||||||
|
const result = errNone("fail").flatMapErr(e => (e === "retry" ? okSome(0) : errNone(e.length)) as Query<never, number>);
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Query<never, number>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("mapErr method", () => {
|
||||||
|
it("should transform ErrNone<E> to Query<T, F> with correct type inference", () => {
|
||||||
|
// Arrange
|
||||||
|
const errQuery = errNone("error");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mapped = errQuery.mapErr((e) => e.toUpperCase());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<ErrNone<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Query<T, F> when called on Query<T, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = errNone("error") as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mapped = query.mapErr((e) => e.length);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<Query<number, number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve T when called on OkSome<T>", () => {
|
||||||
|
// Arrange
|
||||||
|
const query = qi<number, never>(okSome(42) as Query<number, never>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mapped = query.mapErr((e: never) => e);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(qi(mapped)).toEqualTypeOf<QueryInterface<number, never>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with correct mapper function signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const errQuery = errNone("error");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(errQuery.mapErr).toBeCallableWith((e: string) => e.toUpperCase());
|
||||||
|
expectTypeOf(errQuery.mapErr).toBeCallableWith((e: string) => e.length);
|
||||||
|
// @ts-expect-error - wrong parameter type for mapErr callback
|
||||||
|
errQuery.mapErr((e: number) => e.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("map and mapErr interaction", () => {
|
||||||
|
it("should work together in chains with correct type inference", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okSome(42) as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query
|
||||||
|
.map((x) => x.toString())
|
||||||
|
.mapErr((e) => e.length);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Query<string, number>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
366
src/query/spec/match.spec.ts
Normal file
366
src/query/spec/match.spec.ts
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Query, okSome, okNone, errNone } from '../';
|
||||||
|
|
||||||
|
describe('Query.match - Runtime Tests', () => {
|
||||||
|
describe('match with OkSome', () => {
|
||||||
|
it('should execute onSome callback with the contained value', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
const onSome = vi.fn((value: number) => `Value: ${value}`);
|
||||||
|
const onNone = vi.fn(() => 'No value');
|
||||||
|
const onErr = vi.fn((error: never) => `Error: ${error}`);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.match(onSome, onNone, onErr);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(onSome).toHaveBeenCalledWith(42);
|
||||||
|
expect(onSome).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onNone).not.toHaveBeenCalled();
|
||||||
|
expect(onErr).not.toHaveBeenCalled();
|
||||||
|
expect(result).toBe('Value: 42');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the result of onSome callback', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(10);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.match(
|
||||||
|
(value) => value * 2,
|
||||||
|
() => 0,
|
||||||
|
() => -1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with various data types', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
{ query: okSome(42), expected: 'number: 42' },
|
||||||
|
{ query: okSome('hello'), expected: 'string: "hello"' },
|
||||||
|
{ query: okSome(true), expected: 'boolean: true' },
|
||||||
|
{ query: okSome({ id: 1 }), expected: 'object: {"id":1}' },
|
||||||
|
{ query: okSome([1, 2, 3]), expected: 'object: [1,2,3]' },
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ query, expected }) => {
|
||||||
|
// Act
|
||||||
|
const result = query.match(
|
||||||
|
(value) => `${typeof value}: ${JSON.stringify(value)}`,
|
||||||
|
() => 'none',
|
||||||
|
() => 'error',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex transformations', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome({ name: 'John', age: 30 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.match(
|
||||||
|
(person) => ({
|
||||||
|
greeting: `Hello, ${person.name}!`,
|
||||||
|
isAdult: person.age >= 18,
|
||||||
|
}),
|
||||||
|
() => ({ greeting: 'Hello, stranger!', isAdult: false }),
|
||||||
|
() => ({ greeting: 'Error occurred', isAdult: false }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual({
|
||||||
|
greeting: 'Hello, John!',
|
||||||
|
isAdult: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('match with OkNone', () => {
|
||||||
|
it('should execute onNone callback', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
const onSome = vi.fn((value: number) => `Value: ${value}`);
|
||||||
|
const onNone = vi.fn(() => 'No value');
|
||||||
|
const onErr = vi.fn((error: string) => `Error: ${error}`);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.match(onSome, onNone, onErr);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(onNone).toHaveBeenCalledWith();
|
||||||
|
expect(onNone).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onSome).not.toHaveBeenCalled();
|
||||||
|
expect(onErr).not.toHaveBeenCalled();
|
||||||
|
expect(result).toBe('No value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the result of onNone callback', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<string, number> = okNone() as Query<string, number>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.match(
|
||||||
|
(value) => value.toUpperCase(),
|
||||||
|
() => 'DEFAULT',
|
||||||
|
(err) => `Error: ${err}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('DEFAULT');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle different return types from onNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringResult = query.match(
|
||||||
|
(value) => value.toString(),
|
||||||
|
() => 'empty',
|
||||||
|
(err) => err,
|
||||||
|
);
|
||||||
|
|
||||||
|
const numberResult = query.match(
|
||||||
|
(value) => value,
|
||||||
|
() => -1,
|
||||||
|
() => -2,
|
||||||
|
);
|
||||||
|
|
||||||
|
const objectResult = query.match(
|
||||||
|
(value): { value: number | null } => ({ value }),
|
||||||
|
(): { value: number | null } => ({ value: null }),
|
||||||
|
(): { value: number | null } => ({ value: null }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(stringResult).toBe('empty');
|
||||||
|
expect(numberResult).toBe(-1);
|
||||||
|
expect(objectResult).toEqual({ value: null });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('match with ErrNone', () => {
|
||||||
|
it('should execute onErr callback with the contained error', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = errNone('something failed') as Query<number, string>;
|
||||||
|
const onSome = vi.fn((value: number) => `Value: ${value}`);
|
||||||
|
const onNone = vi.fn(() => 'No value');
|
||||||
|
const onErr = vi.fn((error: string) => `Error: ${error}`);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.match(onSome, onNone, onErr);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(onErr).toHaveBeenCalledWith('something failed');
|
||||||
|
expect(onErr).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onSome).not.toHaveBeenCalled();
|
||||||
|
expect(onNone).not.toHaveBeenCalled();
|
||||||
|
expect(result).toBe('Error: something failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the result of onErr callback', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = errNone(404);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.match(
|
||||||
|
() => 'has value',
|
||||||
|
() => 'no value',
|
||||||
|
(err) => `HTTP ${err}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('HTTP 404');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex error objects', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = errNone({ code: 'NOT_FOUND', message: 'Resource not found' });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.match(
|
||||||
|
() => ({ status: 'ok', message: '' }),
|
||||||
|
() => ({ status: 'empty', message: '' }),
|
||||||
|
(err) => ({ status: 'error', message: `${err.code}: ${err.message}` }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual({
|
||||||
|
status: 'error',
|
||||||
|
message: 'NOT_FOUND: Resource not found',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('match with mixed Query types', () => {
|
||||||
|
it('should handle dynamic query types correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<string, string>[] = [
|
||||||
|
okSome('hello') as Query<string, string>,
|
||||||
|
okNone() as Query<string, string>,
|
||||||
|
errNone('error') as Query<string, string>,
|
||||||
|
okSome('world') as Query<string, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const results = queries.map((query) =>
|
||||||
|
query.match(
|
||||||
|
(value) => value.toUpperCase(),
|
||||||
|
() => 'EMPTY',
|
||||||
|
(err) => `ERR: ${err}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results).toEqual(['HELLO', 'EMPTY', 'ERR: error', 'WORLD']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work in conditional contexts', () => {
|
||||||
|
// Arrange
|
||||||
|
const getQuery = (type: 'some' | 'none' | 'err'): Query<number, string> => {
|
||||||
|
if (type === 'some') return okSome(42) as Query<number, string>;
|
||||||
|
if (type === 'none') return okNone() as Query<number, string>;
|
||||||
|
return errNone('error') as Query<number, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const someResult = getQuery('some').match(
|
||||||
|
(value) => `Found: ${value}`,
|
||||||
|
() => 'Not found',
|
||||||
|
(err) => `Error: ${err}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const noneResult = getQuery('none').match(
|
||||||
|
(value) => `Found: ${value}`,
|
||||||
|
() => 'Not found',
|
||||||
|
(err) => `Error: ${err}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const errResult = getQuery('err').match(
|
||||||
|
(value) => `Found: ${value}`,
|
||||||
|
() => 'Not found',
|
||||||
|
(err) => `Error: ${err}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(someResult).toBe('Found: 42');
|
||||||
|
expect(noneResult).toBe('Not found');
|
||||||
|
expect(errResult).toBe('Error: error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('match error handling', () => {
|
||||||
|
it('should propagate errors from onSome callback', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
const error = new Error('onSome error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(() => {
|
||||||
|
query.match(
|
||||||
|
() => {
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
() => 'fallback',
|
||||||
|
() => 'err fallback',
|
||||||
|
);
|
||||||
|
}).toThrow('onSome error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should propagate errors from onNone callback', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
const error = new Error('onNone error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(() => {
|
||||||
|
query.match(
|
||||||
|
(value) => value.toString(),
|
||||||
|
() => {
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
(err) => err,
|
||||||
|
);
|
||||||
|
}).toThrow('onNone error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should propagate errors from onErr callback', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = errNone('error') as Query<number, string>;
|
||||||
|
const error = new Error('onErr error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(() => {
|
||||||
|
query.match(
|
||||||
|
(value) => value.toString(),
|
||||||
|
() => 'none',
|
||||||
|
() => {
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toThrow('onErr error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('match with async callbacks', () => {
|
||||||
|
it('should handle async onSome callback', async () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await query.match(
|
||||||
|
async (value) => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
return `Async: ${value}`;
|
||||||
|
},
|
||||||
|
async () => 'Async: No value',
|
||||||
|
async () => 'Async: Error',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('Async: 42');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle async onNone callback', async () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await query.match(
|
||||||
|
async (value) => `Async: ${value}`,
|
||||||
|
async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
return 'Async: No value';
|
||||||
|
},
|
||||||
|
async (err) => `Async: ${err}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('Async: No value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle async onErr callback', async () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = errNone('fail') as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await query.match(
|
||||||
|
async (value) => `Async: ${value}`,
|
||||||
|
async () => 'Async: No value',
|
||||||
|
async (err) => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
return `Async: ${err}`;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('Async: fail');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
217
src/query/spec/match.type-spec.ts
Normal file
217
src/query/spec/match.type-spec.ts
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Query, OkSome, OkNone, ErrNone, okSome, okNone, errNone } from "../";
|
||||||
|
|
||||||
|
describe("Query.match - Type Tests", () => {
|
||||||
|
describe("match with OkSome", () => {
|
||||||
|
it("should infer return type from all three callback return types", () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringResult = query.match(
|
||||||
|
(value) => `Value: ${value}`,
|
||||||
|
() => "No value",
|
||||||
|
() => "Error",
|
||||||
|
);
|
||||||
|
|
||||||
|
const numberResult = query.match(
|
||||||
|
(value) => value * 2,
|
||||||
|
() => 0,
|
||||||
|
() => -1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<string>();
|
||||||
|
expectTypeOf(numberResult).toEqualTypeOf<number>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should infer union type when callbacks return different types", () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const unionResult = query.match<string | number | boolean>(
|
||||||
|
(value) => value.toString(),
|
||||||
|
() => 0,
|
||||||
|
() => true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(unionResult).toEqualTypeOf<string | number | boolean>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve complex return types", () => {
|
||||||
|
// Arrange
|
||||||
|
type ComplexType = { data: string[]; count: number };
|
||||||
|
const query = okSome({ id: 1, name: "test" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const complexResult = query.match(
|
||||||
|
(value): ComplexType => ({ data: [value.name], count: 1 }),
|
||||||
|
(): ComplexType => ({ data: [], count: 0 }),
|
||||||
|
(): ComplexType => ({ data: [], count: -1 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(complexResult).toEqualTypeOf<ComplexType>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("match with OkNone", () => {
|
||||||
|
it("should infer return type from callback return types", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okNone() as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringResult = query.match(
|
||||||
|
(value) => `Value: ${value}`,
|
||||||
|
() => "No value",
|
||||||
|
(err) => `Error: ${err}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<string>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("match with ErrNone", () => {
|
||||||
|
it("should infer return type from callback return types", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = errNone("error") as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringResult = query.match(
|
||||||
|
(value) => `Value: ${value}`,
|
||||||
|
() => "No value",
|
||||||
|
(err) => `Error: ${err}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<string>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("match callback parameter types", () => {
|
||||||
|
it("should provide correct parameter type to onSome callback", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberQuery = okSome(42);
|
||||||
|
const stringQuery = okSome("hello");
|
||||||
|
const objectQuery = okSome({ id: 1, name: "test" });
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
numberQuery.match(
|
||||||
|
(value) => {
|
||||||
|
expectTypeOf(value).toEqualTypeOf<number>();
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
() => 0,
|
||||||
|
() => 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
stringQuery.match(
|
||||||
|
(value) => {
|
||||||
|
expectTypeOf(value).toEqualTypeOf<string>();
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
() => "",
|
||||||
|
() => "",
|
||||||
|
);
|
||||||
|
|
||||||
|
objectQuery.match(
|
||||||
|
(value) => {
|
||||||
|
expectTypeOf(value).toEqualTypeOf<{ id: number; name: string }>();
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
() => ({ id: 0, name: "" }),
|
||||||
|
() => ({ id: 0, name: "" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should provide correct parameter type to onErr callback", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = errNone("error") as Query<number, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
query.match(
|
||||||
|
(value) => {
|
||||||
|
expectTypeOf(value).toEqualTypeOf<number>();
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
() => "",
|
||||||
|
(err) => {
|
||||||
|
expectTypeOf(err).toEqualTypeOf<string>();
|
||||||
|
return err;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("match function signature validation", () => {
|
||||||
|
it("should be callable with correct callback signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okSome(42) as Query<number, string>;
|
||||||
|
const onSome = (value: number) => value * 2;
|
||||||
|
const onNone = () => 0;
|
||||||
|
const onErr = (err: string) => err.length;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(query.match).toBeCallableWith(onSome, onNone, onErr);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be callable with incorrect callback signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okSome(42) as Query<number, string>;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// @ts-expect-error - wrong parameter type for onSome callback
|
||||||
|
query.match((value: string) => value, () => "", () => "");
|
||||||
|
// @ts-expect-error - wrong parameter type for onErr callback
|
||||||
|
query.match((x: number) => x, () => 0, (err: number) => err);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be callable with missing callbacks", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okSome(42) as Query<number, string>;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// @ts-expect-error - missing onNone and onErr callbacks
|
||||||
|
query.match((x: number) => x);
|
||||||
|
// @ts-expect-error - missing onErr callback
|
||||||
|
query.match((x: number) => x, () => 0);
|
||||||
|
// @ts-expect-error - missing all callbacks
|
||||||
|
query.match();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("match with complex types", () => {
|
||||||
|
it("should handle async callbacks correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const asyncResult = query.match(
|
||||||
|
async (value) => `Async: ${value}`,
|
||||||
|
async () => "Async: No value",
|
||||||
|
async () => "Async: Error",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(asyncResult).toEqualTypeOf<Promise<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle callback functions as return values", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okSome(42) as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const functionResult = query.match(
|
||||||
|
(value) => (x: number) => x + value,
|
||||||
|
() => (x: number) => x,
|
||||||
|
() => (x: number) => x,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(functionResult).toEqualTypeOf<(x: number) => number>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
641
src/query/spec/namespace.spec.ts
Normal file
641
src/query/spec/namespace.spec.ts
Normal file
@@ -0,0 +1,641 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Query, OkSome, OkNone, ErrNone, okSome, okNone, errNone } from '../';
|
||||||
|
|
||||||
|
describe('Query namespace - Runtime Tests', () => {
|
||||||
|
describe('Query.from', () => {
|
||||||
|
it('should return OkSome when function returns a non-null/undefined value', () => {
|
||||||
|
// Act
|
||||||
|
const result = Query.from(() => 42);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkSome for various value types', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
const stringResult = Query.from(() => 'hello');
|
||||||
|
expect(stringResult.isSome()).toBe(true);
|
||||||
|
if (stringResult.isSome()) expect(stringResult.value).toBe('hello');
|
||||||
|
|
||||||
|
const boolResult = Query.from(() => true);
|
||||||
|
expect(boolResult.isSome()).toBe(true);
|
||||||
|
if (boolResult.isSome()) expect(boolResult.value).toBe(true);
|
||||||
|
|
||||||
|
const objResult = Query.from(() => ({ id: 1 }));
|
||||||
|
expect(objResult.isSome()).toBe(true);
|
||||||
|
if (objResult.isSome()) expect(objResult.value).toEqual({ id: 1 });
|
||||||
|
|
||||||
|
const arrResult = Query.from(() => [1, 2, 3]);
|
||||||
|
expect(arrResult.isSome()).toBe(true);
|
||||||
|
if (arrResult.isSome()) expect(arrResult.value).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkNone when function returns null', () => {
|
||||||
|
// Act
|
||||||
|
const result = Query.from(() => null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkNone when function returns undefined', () => {
|
||||||
|
// Act
|
||||||
|
const result = Query.from(() => undefined);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return ErrNone when function throws', () => {
|
||||||
|
// Arrange
|
||||||
|
const error = new Error('something failed');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.from(() => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply errorMap when function throws and errorMap is provided', () => {
|
||||||
|
// Act
|
||||||
|
const result = Query.from(
|
||||||
|
() => {
|
||||||
|
throw new Error('original');
|
||||||
|
},
|
||||||
|
() => 'mapped error',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('mapped error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass the caught error to errorMap', () => {
|
||||||
|
// Arrange
|
||||||
|
const originalError = new Error('original');
|
||||||
|
const errorMapSpy = vi.fn((e: unknown) => `Mapped: ${(e as Error).message}`);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.from(() => {
|
||||||
|
throw originalError;
|
||||||
|
}, errorMapSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(errorMapSpy).toHaveBeenCalledWith(originalError);
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('Mapped: original');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkSome for falsy-but-not-nullish values (0, false, "")', () => {
|
||||||
|
// Act
|
||||||
|
const zeroResult = Query.from(() => 0 as number | null);
|
||||||
|
const falseResult = Query.from(() => false as boolean | null);
|
||||||
|
const emptyResult = Query.from(() => '' as string | null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(zeroResult.isSome()).toBe(true);
|
||||||
|
if (zeroResult.isSome()) expect(zeroResult.value).toBe(0);
|
||||||
|
|
||||||
|
expect(falseResult.isSome()).toBe(true);
|
||||||
|
if (falseResult.isSome()) expect(falseResult.value).toBe(false);
|
||||||
|
|
||||||
|
expect(emptyResult.isSome()).toBe(true);
|
||||||
|
if (emptyResult.isSome()) expect(emptyResult.value).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkSome when passed a non-null/undefined value directly', () => {
|
||||||
|
// Act & Assert
|
||||||
|
const numResult = Query.from(42);
|
||||||
|
expect(numResult.isSome()).toBe(true);
|
||||||
|
if (numResult.isSome()) expect(numResult.value).toBe(42);
|
||||||
|
|
||||||
|
const strResult = Query.from('hello');
|
||||||
|
expect(strResult.isSome()).toBe(true);
|
||||||
|
if (strResult.isSome()) expect(strResult.value).toBe('hello');
|
||||||
|
|
||||||
|
const objResult = Query.from({ id: 1 });
|
||||||
|
expect(objResult.isSome()).toBe(true);
|
||||||
|
if (objResult.isSome()) expect(objResult.value).toEqual({ id: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkNone when passed null or undefined directly', () => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(Query.from(null).isNone()).toBe(true);
|
||||||
|
expect(Query.from(undefined).isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkSome for falsy-but-not-nullish values passed directly', () => {
|
||||||
|
// Act & Assert
|
||||||
|
const zeroResult = Query.from(0);
|
||||||
|
expect(zeroResult.isSome()).toBe(true);
|
||||||
|
if (zeroResult.isSome()) expect(zeroResult.value).toBe(0);
|
||||||
|
|
||||||
|
const falseResult = Query.from(false);
|
||||||
|
expect(falseResult.isSome()).toBe(true);
|
||||||
|
if (falseResult.isSome()) expect(falseResult.value).toBe(false);
|
||||||
|
|
||||||
|
const emptyResult = Query.from('');
|
||||||
|
expect(emptyResult.isSome()).toBe(true);
|
||||||
|
if (emptyResult.isSome()) expect(emptyResult.value).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Query.isQuery', () => {
|
||||||
|
it('should return true for OkSome instances', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(Query.isQuery(okSome(42))).toBe(true);
|
||||||
|
expect(Query.isQuery(okSome('hello'))).toBe(true);
|
||||||
|
expect(Query.isQuery(okSome({ id: 1 }))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for OkNone instances', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(Query.isQuery(okNone())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for ErrNone instances', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(Query.isQuery(errNone('error'))).toBe(true);
|
||||||
|
expect(Query.isQuery(errNone(404))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for non-Query values', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(Query.isQuery(42)).toBe(false);
|
||||||
|
expect(Query.isQuery('hello')).toBe(false);
|
||||||
|
expect(Query.isQuery(null)).toBe(false);
|
||||||
|
expect(Query.isQuery(undefined)).toBe(false);
|
||||||
|
expect(Query.isQuery(true)).toBe(false);
|
||||||
|
expect(Query.isQuery({ isSome: () => true })).toBe(false);
|
||||||
|
expect(Query.isQuery([1, 2, 3])).toBe(false);
|
||||||
|
expect(Query.isQuery({})).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Query.all', () => {
|
||||||
|
it('should return OkSome with all values when all queries are OkSome', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okSome(1) as Query<number, string>,
|
||||||
|
okSome(2) as Query<number, string>,
|
||||||
|
okSome(3) as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.all(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toEqual([1, 2, 3]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip OkNone values and return remaining values', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okSome(1) as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
okSome(3) as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.all(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toEqual([1, 3]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkNone when all queries are OkNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.all(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the first ErrNone when any query is ErrNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okSome(1) as Query<number, string>,
|
||||||
|
errNone('first error') as Query<number, string>,
|
||||||
|
okSome(3) as Query<number, string>,
|
||||||
|
errNone('second error') as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.all(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('first error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkNone for empty array', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.all(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed OkSome and OkNone correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
okSome(1) as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
okSome(2) as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.all(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toEqual([1, 2]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return ErrNone even if preceded by OkNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
errNone('error') as Query<number, string>,
|
||||||
|
okSome(1) as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.all(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Query.partition', () => {
|
||||||
|
it('should partition queries into values, noneCount, and errors', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okSome(1) as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
errNone('error1') as Query<number, string>,
|
||||||
|
okSome(2) as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
errNone('error2') as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.partition(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.values).toEqual([1, 2]);
|
||||||
|
expect(result.noneCount).toBe(2);
|
||||||
|
expect(result.errors).toEqual(['error1', 'error2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty arrays and zero noneCount for empty input', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.partition(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.values).toEqual([]);
|
||||||
|
expect(result.noneCount).toBe(0);
|
||||||
|
expect(result.errors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all OkSome queries', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okSome(1) as Query<number, string>,
|
||||||
|
okSome(2) as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.partition(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.values).toEqual([1, 2]);
|
||||||
|
expect(result.noneCount).toBe(0);
|
||||||
|
expect(result.errors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all OkNone queries', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.partition(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.values).toEqual([]);
|
||||||
|
expect(result.noneCount).toBe(3);
|
||||||
|
expect(result.errors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all ErrNone queries', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
errNone('err1') as Query<number, string>,
|
||||||
|
errNone('err2') as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.partition(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.values).toEqual([]);
|
||||||
|
expect(result.noneCount).toBe(0);
|
||||||
|
expect(result.errors).toEqual(['err1', 'err2']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Query.compact', () => {
|
||||||
|
it('should extract values from OkSome instances only', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okSome(1) as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
errNone('error') as Query<number, string>,
|
||||||
|
okSome(3) as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.compact(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual([1, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array when no OkSome instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
errNone('error') as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.compact(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for empty input', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.compact(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all values when all are OkSome', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okSome(1) as Query<number, string>,
|
||||||
|
okSome(2) as Query<number, string>,
|
||||||
|
okSome(3) as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.compact(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Query.some', () => {
|
||||||
|
it('should return true when at least one query is OkSome', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okSome(1) as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
errNone('error') as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.some(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when at least one query is OkNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
errNone('error') as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.some(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for empty array', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.some(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when all are OkNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.some(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when all are ErrNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
errNone('error1') as Query<number, string>,
|
||||||
|
errNone('error2') as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.some(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when only one query is OkSome', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
okSome(42) as Query<number, string>,
|
||||||
|
errNone('error') as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.some(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Query.every', () => {
|
||||||
|
it('should return true when all queries are OkSome or OkNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okSome(1) as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
okSome(2) as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.every(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when any query is ErrNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okSome(1) as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
errNone('error') as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.every(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for empty array', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.every(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when all are OkSome', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okSome(1) as Query<number, string>,
|
||||||
|
okSome(2) as Query<number, string>,
|
||||||
|
okSome(3) as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.every(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when all are OkNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.every(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when all are ErrNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
errNone('err1') as Query<number, string>,
|
||||||
|
errNone('err2') as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.every(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept both OkSome and OkNone as success (not just OkSome)', () => {
|
||||||
|
// Arrange - all OkNone is still "every"
|
||||||
|
const allNone: Query<number, string>[] = [
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(Query.every(allNone)).toBe(true);
|
||||||
|
|
||||||
|
// Arrange - mixed OkSome and OkNone
|
||||||
|
const mixed: Query<number, string>[] = [
|
||||||
|
okSome(1) as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(Query.every(mixed)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
227
src/query/spec/namespace.type-spec.ts
Normal file
227
src/query/spec/namespace.type-spec.ts
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import {
|
||||||
|
Query,
|
||||||
|
QueryInterface,
|
||||||
|
OkSome,
|
||||||
|
OkNone,
|
||||||
|
ErrNone,
|
||||||
|
okSome,
|
||||||
|
okNone,
|
||||||
|
errNone,
|
||||||
|
QueryPartitionResult,
|
||||||
|
} from "../";
|
||||||
|
|
||||||
|
// Helper to use the interface signatures which correctly unify T and E
|
||||||
|
const qi = <T, E>(q: Query<T, E>) => q as unknown as QueryInterface<T, E>;
|
||||||
|
|
||||||
|
describe("Query namespace - Type Tests", () => {
|
||||||
|
describe("Query.from", () => {
|
||||||
|
it("should return QueryFromType<T> | ErrNone<unknown> without errorMap", () => {
|
||||||
|
// Act
|
||||||
|
const result = Query.from(() => 42);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<OkSome<number> | ErrNone<unknown>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return QueryFromType<T> | ErrNone<E> with errorMap", () => {
|
||||||
|
// Act
|
||||||
|
const result = Query.from(
|
||||||
|
() => 42,
|
||||||
|
() => "error",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<OkSome<number> | ErrNone<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return OkNone for null return type", () => {
|
||||||
|
// Act
|
||||||
|
const result = Query.from(() => null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<OkNone | ErrNone<unknown>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return OkNone for undefined return type", () => {
|
||||||
|
// Act
|
||||||
|
const result = Query.from(() => undefined);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<OkNone | ErrNone<unknown>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle nullable return types", () => {
|
||||||
|
// Act
|
||||||
|
const result = Query.from(() => "hello" as string | null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(qi(result as Query<string, unknown>)).toEqualTypeOf<QueryInterface<string, unknown>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with correct function signatures", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(Query.from).toBeCallableWith(() => 42);
|
||||||
|
expectTypeOf(Query.from).toBeCallableWith(
|
||||||
|
() => 42,
|
||||||
|
() => "error",
|
||||||
|
);
|
||||||
|
expectTypeOf(Query.from).toBeCallableWith(() => null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept plain values and return QueryFromType", () => {
|
||||||
|
// Act
|
||||||
|
const numResult = Query.from(42);
|
||||||
|
const strResult = Query.from("hello");
|
||||||
|
const nullResult = Query.from(null);
|
||||||
|
const undefinedResult = Query.from(undefined);
|
||||||
|
const zeroResult = Query.from(0);
|
||||||
|
const falseResult = Query.from(false);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numResult).toEqualTypeOf<OkSome<number> | OkNone>();
|
||||||
|
expectTypeOf(strResult).toEqualTypeOf<OkSome<string> | OkNone>();
|
||||||
|
expectTypeOf(nullResult).toEqualTypeOf<OkNone>();
|
||||||
|
expectTypeOf(undefinedResult).toEqualTypeOf<OkNone>();
|
||||||
|
expectTypeOf(zeroResult).toEqualTypeOf<OkSome<number> | OkNone>();
|
||||||
|
expectTypeOf(falseResult).toEqualTypeOf<OkSome<boolean> | OkNone>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should strip null/undefined from value union types", () => {
|
||||||
|
// Arrange
|
||||||
|
const value = "hello" as string | null;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.from(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<OkSome<string> | OkNone>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject async functions with a type-level error", () => {
|
||||||
|
// @ts-expect-error — async functions should not be passed to Query.from
|
||||||
|
const result = Query.from(async () => 42);
|
||||||
|
|
||||||
|
expectTypeOf(result).toBeString();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject async functions even with errorMap", () => {
|
||||||
|
// @ts-expect-error — async functions should not be passed to Query.from
|
||||||
|
const result = Query.from(async () => 42, () => "error");
|
||||||
|
|
||||||
|
expectTypeOf(result).toBeString();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Query.isQuery", () => {
|
||||||
|
it("should act as type guard for Query<T, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const value: unknown = okSome(42);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (Query.isQuery(value)) {
|
||||||
|
expectTypeOf(value).toEqualTypeOf<Query<unknown, unknown>>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return boolean", () => {
|
||||||
|
// Act
|
||||||
|
const result = Query.isQuery(42);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<boolean>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with any value", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(Query.isQuery).toBeCallableWith(42);
|
||||||
|
expectTypeOf(Query.isQuery).toBeCallableWith("hello");
|
||||||
|
expectTypeOf(Query.isQuery).toBeCallableWith(null);
|
||||||
|
expectTypeOf(Query.isQuery).toBeCallableWith(undefined);
|
||||||
|
expectTypeOf(Query.isQuery).toBeCallableWith(okSome(42));
|
||||||
|
expectTypeOf(Query.isQuery).toBeCallableWith(okNone());
|
||||||
|
expectTypeOf(Query.isQuery).toBeCallableWith(errNone("error"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Query.all", () => {
|
||||||
|
it("should return Query<T[], E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [okSome(1) as Query<number, string>];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.all(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Query<number[], string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with Query array", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(Query.all).toBeCallableWith([
|
||||||
|
okSome(1) as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return OkSome<T[]> when all inputs are known OkSome", () => {
|
||||||
|
const result = Query.all([okSome(1), okSome(2), okSome(3)]);
|
||||||
|
expectTypeOf(result).toEqualTypeOf<OkSome<number[]>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Query.partition", () => {
|
||||||
|
it("should return QueryPartitionResult<T, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [okSome(1) as Query<number, string>];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.partition(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<QueryPartitionResult<number, string>>();
|
||||||
|
expectTypeOf(result.values).toEqualTypeOf<number[]>();
|
||||||
|
expectTypeOf(result.noneCount).toEqualTypeOf<number>();
|
||||||
|
expectTypeOf(result.errors).toEqualTypeOf<string[]>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Query.compact", () => {
|
||||||
|
it("should return T[]", () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [okSome(1) as Query<number, string>];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.compact(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<number[]>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Query.some", () => {
|
||||||
|
it("should return boolean", () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [okSome(1) as Query<number, string>];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.some(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<boolean>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Query.every", () => {
|
||||||
|
it("should return boolean", () => {
|
||||||
|
// Arrange
|
||||||
|
const queries: Query<number, string>[] = [okSome(1) as Query<number, string>];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Query.every(queries);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<boolean>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
185
src/query/spec/nullable.spec.ts
Normal file
185
src/query/spec/nullable.spec.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Query, okSome, okNone, errNone } from '../';
|
||||||
|
|
||||||
|
describe('Query toNullable and toUndefined - Runtime Tests', () => {
|
||||||
|
describe('toNullable method', () => {
|
||||||
|
describe('when Query is OkSome', () => {
|
||||||
|
it('should return the contained value', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the value for various data types', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(okSome('hello').toNullable()).toBe('hello');
|
||||||
|
expect(okSome(true).toNullable()).toBe(true);
|
||||||
|
expect(okSome({ id: 1 }).toNullable()).toEqual({ id: 1 });
|
||||||
|
expect(okSome([1, 2, 3]).toNullable()).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return falsy values correctly', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(okSome(0).toNullable()).toBe(0);
|
||||||
|
expect(okSome(false).toNullable()).toBe(false);
|
||||||
|
expect(okSome('').toNullable()).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the exact same reference for objects', () => {
|
||||||
|
// Arrange
|
||||||
|
const obj = { id: 1, name: 'test' };
|
||||||
|
const query = okSome(obj);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(obj);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Query is OkNone', () => {
|
||||||
|
it('should return null', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okNone();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Query is ErrNone', () => {
|
||||||
|
it('should return null', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = errNone('error');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null regardless of error type', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(errNone('string error').toNullable()).toBeNull();
|
||||||
|
expect(errNone(404).toNullable()).toBeNull();
|
||||||
|
expect(errNone(new Error('fail')).toNullable()).toBeNull();
|
||||||
|
expect(errNone({ code: 'ERR' }).toNullable()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toUndefined method', () => {
|
||||||
|
describe('when Query is OkSome', () => {
|
||||||
|
it('should return the contained value', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the value for various data types', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(okSome('hello').toUndefined()).toBe('hello');
|
||||||
|
expect(okSome(true).toUndefined()).toBe(true);
|
||||||
|
expect(okSome({ id: 1 }).toUndefined()).toEqual({ id: 1 });
|
||||||
|
expect(okSome([1, 2, 3]).toUndefined()).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return falsy values correctly', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(okSome(0).toUndefined()).toBe(0);
|
||||||
|
expect(okSome(false).toUndefined()).toBe(false);
|
||||||
|
expect(okSome('').toUndefined()).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the exact same reference for objects', () => {
|
||||||
|
// Arrange
|
||||||
|
const obj = { id: 1, name: 'test' };
|
||||||
|
const query = okSome(obj);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(obj);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Query is OkNone', () => {
|
||||||
|
it('should return undefined', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okNone();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Query is ErrNone', () => {
|
||||||
|
it('should return undefined', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = errNone('error');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined regardless of error type', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(errNone('string error').toUndefined()).toBeUndefined();
|
||||||
|
expect(errNone(404).toUndefined()).toBeUndefined();
|
||||||
|
expect(errNone(new Error('fail')).toUndefined()).toBeUndefined();
|
||||||
|
expect(errNone({ code: 'ERR' }).toUndefined()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toNullable and toUndefined consistency', () => {
|
||||||
|
it('should both return the value for OkSome', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okSome(42);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(query.toNullable()).toBe(42);
|
||||||
|
expect(query.toUndefined()).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should both return non-value for OkNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okNone();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(query.toNullable()).toBeNull();
|
||||||
|
expect(query.toUndefined()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should both return non-value for ErrNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const query = errNone('error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(query.toNullable()).toBeNull();
|
||||||
|
expect(query.toUndefined()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
108
src/query/spec/nullable.type-spec.ts
Normal file
108
src/query/spec/nullable.type-spec.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Query, OkSome, OkNone, ErrNone, okSome, okNone, errNone } from "../";
|
||||||
|
|
||||||
|
describe("Query toNullable and toUndefined - Type Tests", () => {
|
||||||
|
describe("toNullable method", () => {
|
||||||
|
it("should return T for OkSome<T>", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberQuery = okSome(42);
|
||||||
|
const stringQuery = okSome("hello");
|
||||||
|
const objectQuery = okSome({ id: 1 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberResult = numberQuery.toNullable();
|
||||||
|
const stringResult = stringQuery.toNullable();
|
||||||
|
const objectResult = objectQuery.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberResult).toEqualTypeOf<number>();
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<string>();
|
||||||
|
expectTypeOf(objectResult).toEqualTypeOf<{ id: number }>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null for OkNone", () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okNone();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<null>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null for ErrNone<E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const query = errNone("error");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<null>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return T | null for Query<T, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okSome(42) as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<number | null>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toUndefined method", () => {
|
||||||
|
it("should return T for OkSome<T>", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberQuery = okSome(42);
|
||||||
|
const stringQuery = okSome("hello");
|
||||||
|
const objectQuery = okSome({ id: 1 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberResult = numberQuery.toUndefined();
|
||||||
|
const stringResult = stringQuery.toUndefined();
|
||||||
|
const objectResult = objectQuery.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberResult).toEqualTypeOf<number>();
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<string>();
|
||||||
|
expectTypeOf(objectResult).toEqualTypeOf<{ id: number }>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined for OkNone", () => {
|
||||||
|
// Arrange
|
||||||
|
const query = okNone();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<undefined>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined for ErrNone<E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const query = errNone("error");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<undefined>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return T | undefined for Query<T, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okSome(42) as Query<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = query.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<number | undefined>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
472
src/query/spec/promise.spec.ts
Normal file
472
src/query/spec/promise.spec.ts
Normal file
@@ -0,0 +1,472 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Query, QueryPromise, okSome, okNone, errNone } from '../';
|
||||||
|
|
||||||
|
describe('QueryPromise namespace - Runtime Tests', () => {
|
||||||
|
describe('QueryPromise.from', () => {
|
||||||
|
it('should return OkSome when promise resolves with non-null/undefined value', async () => {
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.from(() => Promise.resolve(42));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkSome for various value types', async () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
const stringResult = await QueryPromise.from(() => Promise.resolve('hello'));
|
||||||
|
expect(stringResult.isSome()).toBe(true);
|
||||||
|
if (stringResult.isSome()) expect(stringResult.value).toBe('hello');
|
||||||
|
|
||||||
|
const boolResult = await QueryPromise.from(() => Promise.resolve(true));
|
||||||
|
expect(boolResult.isSome()).toBe(true);
|
||||||
|
if (boolResult.isSome()) expect(boolResult.value).toBe(true);
|
||||||
|
|
||||||
|
const objResult = await QueryPromise.from(() => Promise.resolve({ id: 1 }));
|
||||||
|
expect(objResult.isSome()).toBe(true);
|
||||||
|
if (objResult.isSome()) expect(objResult.value).toEqual({ id: 1 });
|
||||||
|
|
||||||
|
const arrResult = await QueryPromise.from(() => Promise.resolve([1, 2, 3]));
|
||||||
|
expect(arrResult.isSome()).toBe(true);
|
||||||
|
if (arrResult.isSome()) expect(arrResult.value).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkNone when promise resolves with null', async () => {
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.from(() => Promise.resolve(null));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkNone when promise resolves with undefined', async () => {
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.from(() => Promise.resolve(undefined));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return ErrNone when promise rejects', async () => {
|
||||||
|
// Arrange
|
||||||
|
const error = new Error('async failure');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.from(() => Promise.reject(error));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return ErrNone when function throws synchronously', async () => {
|
||||||
|
// Arrange
|
||||||
|
const error = new Error('sync failure');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.from(() => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply errorMap when promise rejects and errorMap is provided', async () => {
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.from(
|
||||||
|
() => Promise.reject(new Error('original')),
|
||||||
|
() => 'mapped error',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('mapped error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass the caught error to errorMap', async () => {
|
||||||
|
// Arrange
|
||||||
|
const originalError = new Error('original');
|
||||||
|
const errorMapSpy = vi.fn((e: unknown) => `Mapped: ${(e as Error).message}`);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.from(() => Promise.reject(originalError), errorMapSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(errorMapSpy).toHaveBeenCalledWith(originalError);
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('Mapped: original');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkSome for falsy-but-not-nullish values (0, false, "")', async () => {
|
||||||
|
// Act
|
||||||
|
const zeroResult = await QueryPromise.from(() => Promise.resolve(0 as number | null));
|
||||||
|
const falseResult = await QueryPromise.from(() => Promise.resolve(false as boolean | null));
|
||||||
|
const emptyResult = await QueryPromise.from(() => Promise.resolve('' as string | null));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(zeroResult.isSome()).toBe(true);
|
||||||
|
if (zeroResult.isSome()) expect(zeroResult.value).toBe(0);
|
||||||
|
|
||||||
|
expect(falseResult.isSome()).toBe(true);
|
||||||
|
if (falseResult.isSome()) expect(falseResult.value).toBe(false);
|
||||||
|
|
||||||
|
expect(emptyResult.isSome()).toBe(true);
|
||||||
|
if (emptyResult.isSome()) expect(emptyResult.value).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('QueryPromise.all', () => {
|
||||||
|
it('should return OkSome with all values when all promises resolve to OkSome', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
Promise.resolve(okSome(2) as Query<number, string>),
|
||||||
|
Promise.resolve(okSome(3) as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toEqual([1, 2, 3]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip OkNone values and return remaining values', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
Promise.resolve(okNone() as Query<number, string>),
|
||||||
|
Promise.resolve(okSome(3) as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
if (result.isSome()) {
|
||||||
|
expect(result.value).toEqual([1, 3]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkNone when all promises resolve to OkNone', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okNone() as Query<number, string>),
|
||||||
|
Promise.resolve(okNone() as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the first ErrNone when any promise resolves to ErrNone', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
Promise.resolve(errNone('first error') as Query<number, string>),
|
||||||
|
Promise.resolve(okSome(3) as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('first error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OkNone for empty array', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Query<number, string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject if any input promise rejects', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
Promise.reject(new Error('promise rejection')),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(QueryPromise.all(promises)).rejects.toThrow('promise rejection');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('QueryPromise.partition', () => {
|
||||||
|
it('should partition promises into values, noneCount, and errors', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
Promise.resolve(okNone() as Query<number, string>),
|
||||||
|
Promise.resolve(errNone('error1') as Query<number, string>),
|
||||||
|
Promise.resolve(okSome(2) as Query<number, string>),
|
||||||
|
Promise.resolve(okNone() as Query<number, string>),
|
||||||
|
Promise.resolve(errNone('error2') as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.partition(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.values).toEqual([1, 2]);
|
||||||
|
expect(result.noneCount).toBe(2);
|
||||||
|
expect(result.errors).toEqual(['error1', 'error2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty arrays and zero noneCount for empty input', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Query<number, string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.partition(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.values).toEqual([]);
|
||||||
|
expect(result.noneCount).toBe(0);
|
||||||
|
expect(result.errors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all OkSome promises', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
Promise.resolve(okSome(2) as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.partition(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.values).toEqual([1, 2]);
|
||||||
|
expect(result.noneCount).toBe(0);
|
||||||
|
expect(result.errors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject if any input promise rejects', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
Promise.reject(new Error('promise rejection')),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(QueryPromise.partition(promises)).rejects.toThrow('promise rejection');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('QueryPromise.compact', () => {
|
||||||
|
it('should extract values from OkSome promises only', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
Promise.resolve(okNone() as Query<number, string>),
|
||||||
|
Promise.resolve(errNone('error') as Query<number, string>),
|
||||||
|
Promise.resolve(okSome(3) as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual([1, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array when no OkSome instances', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okNone() as Query<number, string>),
|
||||||
|
Promise.resolve(errNone('error') as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for empty input', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Query<number, string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject if any input promise rejects', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
Promise.reject(new Error('promise rejection')),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(QueryPromise.compact(promises)).rejects.toThrow('promise rejection');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('QueryPromise.some', () => {
|
||||||
|
it('should return true when at least one promise resolves to OkSome', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
Promise.resolve(okNone() as Query<number, string>),
|
||||||
|
Promise.resolve(errNone('error') as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.some(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when at least one promise resolves to OkNone', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okNone() as Query<number, string>),
|
||||||
|
Promise.resolve(errNone('error') as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.some(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when all promises resolve to ErrNone', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(errNone('error1') as Query<number, string>),
|
||||||
|
Promise.resolve(errNone('error2') as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.some(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for empty array', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Query<number, string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.some(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject if any input promise rejects', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
Promise.reject(new Error('promise rejection')),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(QueryPromise.some(promises)).rejects.toThrow('promise rejection');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('QueryPromise.every', () => {
|
||||||
|
it('should return true when all promises resolve to OkSome or OkNone', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
Promise.resolve(okNone() as Query<number, string>),
|
||||||
|
Promise.resolve(okSome(2) as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.every(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when any promise resolves to ErrNone', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
Promise.resolve(okNone() as Query<number, string>),
|
||||||
|
Promise.resolve(errNone('error') as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.every(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for empty array', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Query<number, string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.every(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when all are OkNone', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okNone() as Query<number, string>),
|
||||||
|
Promise.resolve(okNone() as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await QueryPromise.every(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject if any input promise rejects', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
Promise.reject(new Error('promise rejection')),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(QueryPromise.every(promises)).rejects.toThrow('promise rejection');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
156
src/query/spec/promise.type-spec.ts
Normal file
156
src/query/spec/promise.type-spec.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import {
|
||||||
|
Query,
|
||||||
|
QueryInterface,
|
||||||
|
QueryPromise,
|
||||||
|
OkSome,
|
||||||
|
OkNone,
|
||||||
|
ErrNone,
|
||||||
|
okSome,
|
||||||
|
okNone,
|
||||||
|
errNone,
|
||||||
|
QueryPartitionResult,
|
||||||
|
} from "../";
|
||||||
|
|
||||||
|
// Helper to use the interface signatures which correctly unify T and E
|
||||||
|
const qi = <T, E>(q: Query<T, E>) => q as unknown as QueryInterface<T, E>;
|
||||||
|
|
||||||
|
describe("QueryPromise namespace - Type Tests", () => {
|
||||||
|
describe("QueryPromise.from", () => {
|
||||||
|
it("should return Promise<QueryFromType<T> | ErrNone<unknown>> without errorMap", () => {
|
||||||
|
// Act
|
||||||
|
const result = QueryPromise.from(() => Promise.resolve(42));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<OkSome<number> | ErrNone<unknown>>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Promise<QueryFromType<T> | ErrNone<E>> with errorMap", () => {
|
||||||
|
// Act
|
||||||
|
const result = QueryPromise.from(
|
||||||
|
() => Promise.resolve(42),
|
||||||
|
() => "error",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<OkSome<number> | ErrNone<string>>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return OkNone for null promise type", () => {
|
||||||
|
// Act
|
||||||
|
const result = QueryPromise.from(() => Promise.resolve(null));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<OkNone | ErrNone<unknown>>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return OkNone for undefined promise type", () => {
|
||||||
|
// Act
|
||||||
|
const result = QueryPromise.from(() => Promise.resolve(undefined));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<OkNone | ErrNone<unknown>>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle nullable promise return types", () => {
|
||||||
|
// Act
|
||||||
|
const result = QueryPromise.from(() => Promise.resolve("hello" as string | null));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).resolves.toMatchTypeOf<Query<string, unknown>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with correct function signatures", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(QueryPromise.from).toBeCallableWith(() => Promise.resolve(42));
|
||||||
|
expectTypeOf(QueryPromise.from).toBeCallableWith(
|
||||||
|
() => Promise.resolve(42),
|
||||||
|
() => "error",
|
||||||
|
);
|
||||||
|
expectTypeOf(QueryPromise.from).toBeCallableWith(() => Promise.resolve(null));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("QueryPromise.all", () => {
|
||||||
|
it("should return Promise<Query<T[], E>>", () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Query<number, string>>[] = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = QueryPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<Query<number[], string>>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with Promise<Query<T, E>>[]", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(QueryPromise.all).toBeCallableWith([
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
Promise.resolve(okNone() as Query<number, string>),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("QueryPromise.partition", () => {
|
||||||
|
it("should return Promise<QueryPartitionResult<T, E>>", () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Query<number, string>>[] = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = QueryPromise.partition(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<QueryPartitionResult<number, string>>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("QueryPromise.compact", () => {
|
||||||
|
it("should return Promise<T[]>", () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Query<number, string>>[] = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = QueryPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<number[]>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("QueryPromise.some", () => {
|
||||||
|
it("should return Promise<boolean>", () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Query<number, string>>[] = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = QueryPromise.some(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<boolean>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("QueryPromise.every", () => {
|
||||||
|
it("should return Promise<boolean>", () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Query<number, string>>[] = [
|
||||||
|
Promise.resolve(okSome(1) as Query<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = QueryPromise.every(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<boolean>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
389
src/query/spec/query.spec.ts
Normal file
389
src/query/spec/query.spec.ts
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Query, OkSome, OkNone, ErrNone, okSome, okNone, errNone } from '../';
|
||||||
|
|
||||||
|
describe('Query constructors and factories - Runtime Tests', () => {
|
||||||
|
describe('OkSome constructor', () => {
|
||||||
|
it('should create OkSome instance with provided value', () => {
|
||||||
|
// Arrange
|
||||||
|
const value = 42;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = new OkSome(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(OkSome);
|
||||||
|
expect(result.value).toBe(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should store different types of values correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const numberValue = 42;
|
||||||
|
const stringValue = 'hello';
|
||||||
|
const objectValue = { id: 1, name: 'test' };
|
||||||
|
const arrayValue = [1, 2, 3];
|
||||||
|
const functionValue = (x: number) => x * 2;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberOkSome = new OkSome(numberValue);
|
||||||
|
const stringOkSome = new OkSome(stringValue);
|
||||||
|
const objectOkSome = new OkSome(objectValue);
|
||||||
|
const arrayOkSome = new OkSome(arrayValue);
|
||||||
|
const functionOkSome = new OkSome(functionValue);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(numberOkSome.value).toBe(numberValue);
|
||||||
|
expect(stringOkSome.value).toBe(stringValue);
|
||||||
|
expect(objectOkSome.value).toBe(objectValue);
|
||||||
|
expect(arrayOkSome.value).toBe(arrayValue);
|
||||||
|
expect(functionOkSome.value).toBe(functionValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should implement QueryInterface methods', () => {
|
||||||
|
// Arrange
|
||||||
|
const instance = new OkSome(42);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(typeof instance.isSome).toBe('function');
|
||||||
|
expect(typeof instance.isNone).toBe('function');
|
||||||
|
expect(typeof instance.isErr).toBe('function');
|
||||||
|
expect(typeof instance.map).toBe('function');
|
||||||
|
expect(typeof instance.mapErr).toBe('function');
|
||||||
|
expect(typeof instance.flatMap).toBe('function');
|
||||||
|
expect(typeof instance.flatMapErr).toBe('function');
|
||||||
|
expect(typeof instance.match).toBe('function');
|
||||||
|
expect(typeof instance.filter).toBe('function');
|
||||||
|
expect(typeof instance.toNullable).toBe('function');
|
||||||
|
expect(typeof instance.toUndefined).toBe('function');
|
||||||
|
expect(typeof instance.toString).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly identify as OkSome', () => {
|
||||||
|
// Arrange
|
||||||
|
const instance = new OkSome(42);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(instance.isSome()).toBe(true);
|
||||||
|
expect(instance.isNone()).toBe(false);
|
||||||
|
expect(instance.isErr()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OkNone constructor', () => {
|
||||||
|
it('should create OkNone instance', () => {
|
||||||
|
// Act
|
||||||
|
const result = new OkNone();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(OkNone);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should implement QueryInterface methods', () => {
|
||||||
|
// Arrange
|
||||||
|
const instance = new OkNone();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(typeof instance.isSome).toBe('function');
|
||||||
|
expect(typeof instance.isNone).toBe('function');
|
||||||
|
expect(typeof instance.isErr).toBe('function');
|
||||||
|
expect(typeof instance.map).toBe('function');
|
||||||
|
expect(typeof instance.mapErr).toBe('function');
|
||||||
|
expect(typeof instance.flatMap).toBe('function');
|
||||||
|
expect(typeof instance.flatMapErr).toBe('function');
|
||||||
|
expect(typeof instance.match).toBe('function');
|
||||||
|
expect(typeof instance.filter).toBe('function');
|
||||||
|
expect(typeof instance.toNullable).toBe('function');
|
||||||
|
expect(typeof instance.toUndefined).toBe('function');
|
||||||
|
expect(typeof instance.toString).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly identify as OkNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const instance = new OkNone();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(instance.isSome()).toBe(false);
|
||||||
|
expect(instance.isNone()).toBe(true);
|
||||||
|
expect(instance.isErr()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ErrNone constructor', () => {
|
||||||
|
it('should create ErrNone instance with provided error', () => {
|
||||||
|
// Arrange
|
||||||
|
const error = 'something went wrong';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = new ErrNone(error);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(ErrNone);
|
||||||
|
expect(result.err).toBe(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should store different types of errors correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const stringError = 'error';
|
||||||
|
const numberError = 404;
|
||||||
|
const objectError = { code: 'NOT_FOUND', message: 'not found' };
|
||||||
|
const errorInstance = new Error('fail');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringErrNone = new ErrNone(stringError);
|
||||||
|
const numberErrNone = new ErrNone(numberError);
|
||||||
|
const objectErrNone = new ErrNone(objectError);
|
||||||
|
const errorErrNone = new ErrNone(errorInstance);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(stringErrNone.err).toBe(stringError);
|
||||||
|
expect(numberErrNone.err).toBe(numberError);
|
||||||
|
expect(objectErrNone.err).toBe(objectError);
|
||||||
|
expect(errorErrNone.err).toBe(errorInstance);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should implement QueryInterface methods', () => {
|
||||||
|
// Arrange
|
||||||
|
const instance = new ErrNone('error');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(typeof instance.isSome).toBe('function');
|
||||||
|
expect(typeof instance.isNone).toBe('function');
|
||||||
|
expect(typeof instance.isErr).toBe('function');
|
||||||
|
expect(typeof instance.map).toBe('function');
|
||||||
|
expect(typeof instance.mapErr).toBe('function');
|
||||||
|
expect(typeof instance.flatMap).toBe('function');
|
||||||
|
expect(typeof instance.flatMapErr).toBe('function');
|
||||||
|
expect(typeof instance.match).toBe('function');
|
||||||
|
expect(typeof instance.filter).toBe('function');
|
||||||
|
expect(typeof instance.toNullable).toBe('function');
|
||||||
|
expect(typeof instance.toUndefined).toBe('function');
|
||||||
|
expect(typeof instance.toString).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly identify as ErrNone', () => {
|
||||||
|
// Arrange
|
||||||
|
const instance = new ErrNone('error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(instance.isSome()).toBe(false);
|
||||||
|
expect(instance.isNone()).toBe(false);
|
||||||
|
expect(instance.isErr()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('okSome() factory function', () => {
|
||||||
|
it('should create OkSome instance with provided value', () => {
|
||||||
|
// Arrange
|
||||||
|
const value = 42;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = okSome(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(OkSome);
|
||||||
|
expect(result.value).toBe(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with various data types', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
42,
|
||||||
|
'hello',
|
||||||
|
true,
|
||||||
|
{ id: 1, name: 'test' },
|
||||||
|
[1, 2, 3],
|
||||||
|
(x: number) => x * 2,
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach((testValue) => {
|
||||||
|
// Act
|
||||||
|
const result = okSome(testValue);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(OkSome);
|
||||||
|
expect(result.value).toBe(testValue);
|
||||||
|
expect(result.isSome()).toBe(true);
|
||||||
|
expect(result.isNone()).toBe(false);
|
||||||
|
expect(result.isErr()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create new instances for each call', () => {
|
||||||
|
// Arrange
|
||||||
|
const value = 42;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const first = okSome(value);
|
||||||
|
const second = okSome(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(first).toBeInstanceOf(OkSome);
|
||||||
|
expect(second).toBeInstanceOf(OkSome);
|
||||||
|
expect(first).not.toBe(second);
|
||||||
|
expect(first.value).toBe(second.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('okNone() factory function', () => {
|
||||||
|
it('should return OkNone instance', () => {
|
||||||
|
// Act
|
||||||
|
const result = okNone();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(OkNone);
|
||||||
|
expect(result.isSome()).toBe(false);
|
||||||
|
expect(result.isNone()).toBe(true);
|
||||||
|
expect(result.isErr()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the same singleton instance', () => {
|
||||||
|
// Act
|
||||||
|
const first = okNone();
|
||||||
|
const second = okNone();
|
||||||
|
const third = okNone();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(first).toBe(second);
|
||||||
|
expect(second).toBe(third);
|
||||||
|
expect(first).toBe(third);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a frozen instance', () => {
|
||||||
|
// Act
|
||||||
|
const instance = okNone();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(Object.isFrozen(instance)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not accept any arguments', () => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(() => okNone()).not.toThrow();
|
||||||
|
|
||||||
|
// @ts-expect-error Testing runtime behavior when called incorrectly
|
||||||
|
expect(() => okNone(42)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('errNone() factory function', () => {
|
||||||
|
it('should create ErrNone instance with provided error', () => {
|
||||||
|
// Arrange
|
||||||
|
const error = 'something failed';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = errNone(error);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(ErrNone);
|
||||||
|
expect(result.err).toBe(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with various error types', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
'error string',
|
||||||
|
42,
|
||||||
|
new Error('fail'),
|
||||||
|
{ code: 'ERR', message: 'failed' },
|
||||||
|
['err1', 'err2'],
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach((testError) => {
|
||||||
|
// Act
|
||||||
|
const result = errNone(testError);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(ErrNone);
|
||||||
|
expect(result.err).toBe(testError);
|
||||||
|
expect(result.isSome()).toBe(false);
|
||||||
|
expect(result.isNone()).toBe(false);
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create new instances for each call', () => {
|
||||||
|
// Arrange
|
||||||
|
const error = 'error';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const first = errNone(error);
|
||||||
|
const second = errNone(error);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(first).not.toBe(second);
|
||||||
|
expect(first.err).toBe(second.err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toString()', () => {
|
||||||
|
it('should return correct string for OkSome', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(okSome(42).toString()).toBe('OkSome(42)');
|
||||||
|
expect(okSome('hello').toString()).toBe('OkSome(hello)');
|
||||||
|
expect(okSome(true).toString()).toBe('OkSome(true)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct string for OkNone', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(okNone().toString()).toBe('OkNone()');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct string for ErrNone', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(errNone('error').toString()).toBe('ErrNone(error)');
|
||||||
|
expect(errNone(404).toString()).toBe('ErrNone(404)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Integration tests', () => {
|
||||||
|
it('should allow creating mixed arrays of Queries', () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const queries: Query<number, string>[] = [
|
||||||
|
okSome(1) as Query<number, string>,
|
||||||
|
okSome(2) as Query<number, string>,
|
||||||
|
okNone() as Query<number, string>,
|
||||||
|
errNone('error') as Query<number, string>,
|
||||||
|
okSome(3) as Query<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(queries).toHaveLength(5);
|
||||||
|
expect(queries[0].isSome()).toBe(true);
|
||||||
|
expect(queries[1].isSome()).toBe(true);
|
||||||
|
expect(queries[2].isNone()).toBe(true);
|
||||||
|
expect(queries[3].isErr()).toBe(true);
|
||||||
|
expect(queries[4].isSome()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable type narrowing in runtime', () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<string, number> = okSome('hello') as Query<string, number>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (query.isSome()) {
|
||||||
|
expect(query.value).toBe('hello');
|
||||||
|
expect(typeof query.value).toBe('string');
|
||||||
|
} else {
|
||||||
|
fail('Expected query to be OkSome');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle edge cases correctly', () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const zeroOkSome = okSome(0);
|
||||||
|
const falseOkSome = okSome(false);
|
||||||
|
const emptyOkSome = okSome('');
|
||||||
|
const emptyArrayOkSome = okSome([]);
|
||||||
|
|
||||||
|
// Assert - All should be OkSome instances even with "falsy" values
|
||||||
|
expect(zeroOkSome.isSome()).toBe(true);
|
||||||
|
expect(falseOkSome.isSome()).toBe(true);
|
||||||
|
expect(emptyOkSome.isSome()).toBe(true);
|
||||||
|
expect(emptyArrayOkSome.isSome()).toBe(true);
|
||||||
|
|
||||||
|
expect(zeroOkSome.value).toBe(0);
|
||||||
|
expect(falseOkSome.value).toBe(false);
|
||||||
|
expect(emptyOkSome.value).toBe('');
|
||||||
|
expect(emptyArrayOkSome.value).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
225
src/query/spec/query.type-spec.ts
Normal file
225
src/query/spec/query.type-spec.ts
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Query, OkSome, OkNone, ErrNone, okSome, okNone, errNone, QueryInterface } from "../";
|
||||||
|
|
||||||
|
describe("Query constructors and factories - Type Tests", () => {
|
||||||
|
describe("OkSome constructor", () => {
|
||||||
|
it("should create OkSome<T> with correct type inference", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const numberOkSome = new OkSome(42);
|
||||||
|
const stringOkSome = new OkSome("hello");
|
||||||
|
const objectOkSome = new OkSome({ id: 1, name: "test" });
|
||||||
|
const arrayOkSome = new OkSome([1, 2, 3]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberOkSome).toEqualTypeOf<OkSome<number>>();
|
||||||
|
expectTypeOf(stringOkSome).toEqualTypeOf<OkSome<string>>();
|
||||||
|
expectTypeOf(objectOkSome).toEqualTypeOf<OkSome<{ id: number; name: string }>>();
|
||||||
|
expectTypeOf(arrayOkSome).toEqualTypeOf<OkSome<number[]>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should implement QueryInterface with correct type parameters", () => {
|
||||||
|
// Arrange
|
||||||
|
const instance = new OkSome(42);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(instance).toMatchTypeOf<QueryInterface<number, never>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be assignable to Query<T, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberOkSome = new OkSome(42);
|
||||||
|
const stringOkSome = new OkSome("hello");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberOkSome).toMatchTypeOf<Query<number, string>>();
|
||||||
|
expectTypeOf(stringOkSome).toMatchTypeOf<Query<string, number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve complex generic types", () => {
|
||||||
|
// Arrange
|
||||||
|
type ComplexType = { data: Array<{ id: number; tags: string[] }> };
|
||||||
|
const complexValue: ComplexType = { data: [{ id: 1, tags: ["a", "b"] }] };
|
||||||
|
const complexOkSome = new OkSome(complexValue);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(complexOkSome).toEqualTypeOf<OkSome<ComplexType>>();
|
||||||
|
expectTypeOf(complexOkSome).toMatchTypeOf<Query<ComplexType, any>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("OkNone constructor", () => {
|
||||||
|
it("should create OkNone type", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const instance = new OkNone();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(instance).toEqualTypeOf<OkNone>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should implement QueryInterface<never, never>", () => {
|
||||||
|
// Arrange
|
||||||
|
const instance = new OkNone();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(instance).toMatchTypeOf<QueryInterface<never, never>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be assignable to Query<T, E> for any T and E", () => {
|
||||||
|
// Arrange
|
||||||
|
const instance = new OkNone();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(instance).toMatchTypeOf<Query<number, string>>();
|
||||||
|
expectTypeOf(instance).toMatchTypeOf<Query<string, number>>();
|
||||||
|
expectTypeOf(instance).toMatchTypeOf<Query<any, any>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ErrNone constructor", () => {
|
||||||
|
it("should create ErrNone<E> with correct type inference", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const stringErr = new ErrNone("error");
|
||||||
|
const numberErr = new ErrNone(404);
|
||||||
|
const objectErr = new ErrNone({ code: "ERR", message: "fail" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringErr).toEqualTypeOf<ErrNone<string>>();
|
||||||
|
expectTypeOf(numberErr).toEqualTypeOf<ErrNone<number>>();
|
||||||
|
expectTypeOf(objectErr).toEqualTypeOf<ErrNone<{ code: string; message: string }>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should implement QueryInterface<never, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const instance = new ErrNone("error");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(instance).toMatchTypeOf<QueryInterface<never, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be assignable to Query<T, E> for any T", () => {
|
||||||
|
// Arrange
|
||||||
|
const instance = new ErrNone("error");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(instance).toMatchTypeOf<Query<number, string>>();
|
||||||
|
expectTypeOf(instance).toMatchTypeOf<Query<any, string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("okSome() factory function", () => {
|
||||||
|
it("should infer correct return type from value", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const numberOkSome = okSome(42);
|
||||||
|
const stringOkSome = okSome("hello");
|
||||||
|
const booleanOkSome = okSome(true);
|
||||||
|
const objectOkSome = okSome({ id: 1 });
|
||||||
|
const arrayOkSome = okSome([1, 2, 3]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberOkSome).toEqualTypeOf<OkSome<number>>();
|
||||||
|
expectTypeOf(stringOkSome).toEqualTypeOf<OkSome<string>>();
|
||||||
|
expectTypeOf(booleanOkSome).toEqualTypeOf<OkSome<boolean>>();
|
||||||
|
expectTypeOf(objectOkSome).toEqualTypeOf<OkSome<{ id: number }>>();
|
||||||
|
expectTypeOf(arrayOkSome).toEqualTypeOf<OkSome<number[]>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve literal types", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const literalNumber = okSome(42 as const);
|
||||||
|
const literalString = okSome("hello" as const);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(literalNumber).toEqualTypeOf<OkSome<42>>();
|
||||||
|
expectTypeOf(literalString).toEqualTypeOf<OkSome<"hello">>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with any type", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(okSome).toBeCallableWith(42);
|
||||||
|
expectTypeOf(okSome).toBeCallableWith("string");
|
||||||
|
expectTypeOf(okSome).toBeCallableWith({ complex: { nested: "object" } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("okNone() factory function", () => {
|
||||||
|
it("should return OkNone type", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const result = okNone();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<OkNone>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be callable with arguments", () => {
|
||||||
|
// Assert
|
||||||
|
// @ts-expect-error - okNone takes no arguments
|
||||||
|
okNone(42);
|
||||||
|
// @ts-expect-error - okNone takes no arguments
|
||||||
|
okNone("string");
|
||||||
|
// @ts-expect-error - okNone takes no arguments
|
||||||
|
okNone(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be assignable to any Query<T, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const result = okNone();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toMatchTypeOf<Query<number, string>>();
|
||||||
|
expectTypeOf(result).toMatchTypeOf<Query<string, number>>();
|
||||||
|
expectTypeOf(result).toMatchTypeOf<Query<{ any: "object" }, Error>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("errNone() factory function", () => {
|
||||||
|
it("should infer correct return type from error", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const stringErr = errNone("error");
|
||||||
|
const numberErr = errNone(404);
|
||||||
|
const objectErr = errNone({ code: "ERR" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringErr).toEqualTypeOf<ErrNone<string>>();
|
||||||
|
expectTypeOf(numberErr).toEqualTypeOf<ErrNone<number>>();
|
||||||
|
expectTypeOf(objectErr).toEqualTypeOf<ErrNone<{ code: string }>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with any type", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(errNone).toBeCallableWith("error");
|
||||||
|
expectTypeOf(errNone).toBeCallableWith(42);
|
||||||
|
expectTypeOf(errNone).toBeCallableWith(new Error("fail"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be assignable to any Query<T, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const result = errNone("error");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toMatchTypeOf<Query<number, string>>();
|
||||||
|
expectTypeOf(result).toMatchTypeOf<Query<any, string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Type narrowing with constructors", () => {
|
||||||
|
it("should enable proper type narrowing with isSome/isNone/isErr", () => {
|
||||||
|
// Arrange
|
||||||
|
const query: Query<number, string> = okSome(42) as Query<number, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (query.isSome()) {
|
||||||
|
expectTypeOf(query).toEqualTypeOf<OkSome<number>>();
|
||||||
|
expectTypeOf(query.value).toEqualTypeOf<number>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.isNone()) {
|
||||||
|
expectTypeOf(query).toEqualTypeOf<OkNone>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.isErr()) {
|
||||||
|
expectTypeOf(query).toEqualTypeOf<ErrNone<string>>();
|
||||||
|
expectTypeOf(query.err).toEqualTypeOf<string>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
566
src/result/index.ts
Normal file
566
src/result/index.ts
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
/**
|
||||||
|
* @template T The type of the success value.
|
||||||
|
* @template E The type of the error value.
|
||||||
|
*/
|
||||||
|
export interface ResultInterface<T, E> {
|
||||||
|
/**
|
||||||
|
* Determines whether this result is successful.
|
||||||
|
* @returns `true` if this result is Ok, `false` otherwise.
|
||||||
|
* ```ts
|
||||||
|
* ok(42).isOk() // true
|
||||||
|
* err('error').isOk() // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
isOk(): this is Ok<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether this result is an error.
|
||||||
|
* @returns `true` if this result is Err, `false` otherwise.
|
||||||
|
* ```ts
|
||||||
|
* ok(42).isErr() // false
|
||||||
|
* err('error').isErr() // true
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
isErr(): this is Err<E>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the contained value by applying a function to it.
|
||||||
|
* @param fn Function to apply to the contained value.
|
||||||
|
* @returns A new result containing the transformed value, or the original Err unchanged.
|
||||||
|
* @remarks When called on a known `Ok<T>`, returns `Ok<U>`. When called on a known `Err<E>`, returns `Err<E>` — the function is never invoked.
|
||||||
|
* ```ts
|
||||||
|
* ok(5).map(x => x * 2) // Ok(10)
|
||||||
|
* err('error').map(x => x * 2) // Err('error')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
map<U>(fn: (value: T) => U): Result<U, E>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the contained error by applying a function to it.
|
||||||
|
* @param fn Function to apply to the contained error.
|
||||||
|
* @returns A new result containing the transformed error, or the original Ok unchanged.
|
||||||
|
* @remarks When called on a known `Err<E>`, returns `Err<F>`. When called on a known `Ok<T>`, returns `Ok<T>` — the function is never invoked.
|
||||||
|
* ```ts
|
||||||
|
* ok(42).mapErr(e => e.toUpperCase()) // Ok(42)
|
||||||
|
* err('error').mapErr(e => e.toUpperCase()) // Err('ERROR')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
mapErr<F>(fn: (error: E) => F): Result<T, F>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a function that returns a result to the contained value and flattens the result.
|
||||||
|
* @param fn Function that takes the contained value and returns a result.
|
||||||
|
* @returns The result returned by the function, or Err if this result is an error.
|
||||||
|
* ```ts
|
||||||
|
* ok(5).flatMap(x => ok(x * 2)) // Ok(10)
|
||||||
|
* ok(5).flatMap(x => err('error')) // Err('error')
|
||||||
|
* err('error').flatMap(x => ok(x * 2)) // Err('error')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
flatMap<U>(fn: (value: T) => Result<U, E>): Result<U, E>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a function that returns a result to the contained error and flattens the result.
|
||||||
|
* @param fn Function that takes the contained error and returns a result.
|
||||||
|
* @returns The result returned by the function, or Ok if this result is successful.
|
||||||
|
* ```ts
|
||||||
|
* ok(42).flatMapErr(e => err(e.toUpperCase())) // Ok(42)
|
||||||
|
* err('error').flatMapErr(e => err(e.toUpperCase())) // Err('ERROR')
|
||||||
|
* err('error').flatMapErr(e => ok('recovered')) // Ok('recovered')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
flatMapErr<F>(fn: (error: E) => Result<T, F>): Result<T, F>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes one of two functions based on whether this result is successful or an error.
|
||||||
|
* @param onOk Function to execute if this result is Ok.
|
||||||
|
* @param onErr Function to execute if this result is Err.
|
||||||
|
* @returns The return value of whichever function is executed. `U` is inferred as the union of both callback return types.
|
||||||
|
* ```ts
|
||||||
|
* ok(42).match(x => `Value: ${x}`, e => `Error: ${e}`) // 'Value: 42'
|
||||||
|
* err('error').match(x => `Value: ${x}`, e => `Error: ${e}`) // 'Error: error'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
match<U>(onOk: (value: T) => U, onErr: (error: E) => U): U;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters this result based on a type predicate, narrowing the value type on success.
|
||||||
|
* @param predicate Type predicate function to test the contained value.
|
||||||
|
* @param errorFactory Function to produce an error if the predicate fails.
|
||||||
|
* @returns Ok with the narrowed type if Ok and the predicate passes, Err otherwise. A no-op on Err.
|
||||||
|
* @remarks This overload resolves when the predicate is a type guard (`value is U`), narrowing `T` to `U`.
|
||||||
|
* ```ts
|
||||||
|
* ok(42 as number | string).filter((x): x is number => typeof x === 'number', v => `Not a number: ${v}`) // Ok(42): Result<number, string>
|
||||||
|
* ok('hello' as number | string).filter((x): x is number => typeof x === 'number', v => `Not a number: ${v}`) // Err('Not a number: hello')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
filter<U extends T>(predicate: (value: T) => value is U, errorFactory: (value: T) => E): Result<U, E>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters this result based on a boolean predicate.
|
||||||
|
* @param predicate Function to test the contained value.
|
||||||
|
* @param errorFactory Function to produce an error if the predicate fails.
|
||||||
|
* @returns Ok if Ok and the predicate returns true, Err otherwise. A no-op on Err.
|
||||||
|
* @remarks This overload resolves when the predicate returns `boolean` (not a type guard). The value type `T` is preserved.
|
||||||
|
* ```ts
|
||||||
|
* ok(5).filter(x => x > 3, x => `${x} is too small`) // Ok(5)
|
||||||
|
* ok(2).filter(x => x > 3, x => `${x} is too small`) // Err('2 is too small')
|
||||||
|
* err('error').filter(x => x > 3, x => `${x} is too small`) // Err('error')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
filter(predicate: (value: T) => boolean, errorFactory: (value: T) => E): Result<T, E>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the contained value or null.
|
||||||
|
* @returns The contained value if this result is Ok, null otherwise.
|
||||||
|
* ```ts
|
||||||
|
* ok(42).toNullable() // 42
|
||||||
|
* err('error').toNullable() // null
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
toNullable(): T | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the contained value or undefined.
|
||||||
|
* @returns The contained value if this result is Ok, undefined otherwise.
|
||||||
|
* ```ts
|
||||||
|
* ok(42).toUndefined() // 42
|
||||||
|
* err('error').toUndefined() // undefined
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
toUndefined(): T | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string representation of this result.
|
||||||
|
* @returns A string describing whether this result is Ok or Err, and the contained value or error.
|
||||||
|
* ```ts
|
||||||
|
* ok(42).toString() // 'Ok(42)'
|
||||||
|
* err('error').toString() // 'Err(error)'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a successful result containing a value of type `T`.
|
||||||
|
*
|
||||||
|
* The error type is always `never` — error-side methods (`mapErr`, `flatMapErr`) are no-ops
|
||||||
|
* that return the same `Ok` instance unchanged.
|
||||||
|
* @remarks The contained value is accessible via the `.value` property, either directly or after narrowing with `isOk()`.
|
||||||
|
*/
|
||||||
|
export class Ok<T> implements ResultInterface<T, any> {
|
||||||
|
constructor(readonly value: T) {}
|
||||||
|
|
||||||
|
isOk(): this is Ok<T> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isErr(): this is never {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
map<U>(fn: (value: T) => U): Ok<U> {
|
||||||
|
return ok(fn(this.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
mapErr<F>(): Ok<T> {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
flatMap<R extends Result<any, any>>(fn: (value: T) => R): R {
|
||||||
|
return fn(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
flatMapErr(): this {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
match<U>(onOk: (value: T) => U, onErr: (error: never) => U): U {
|
||||||
|
return onOk(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
filter<U extends T, F>(predicate: (value: T) => value is U, errorFactory: (value: T) => F): Ok<U> | Err<F>;
|
||||||
|
filter<F>(predicate: (value: T) => boolean, errorFactory: (value: T) => F): Ok<T> | Err<F>;
|
||||||
|
filter<F>(predicate: (value: T) => boolean, errorFactory: (value: T) => F): Ok<T> | Err<F> {
|
||||||
|
return predicate(this.value) ? this : err(errorFactory(this.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
toNullable(): T {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
toUndefined(): T {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `Ok(${this.value})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an error result containing an error of type `E`.
|
||||||
|
*
|
||||||
|
* The value type is always `never` — value-side methods (`map`, `flatMap`, `filter`) are no-ops
|
||||||
|
* that return the same `Err` instance unchanged.
|
||||||
|
* @remarks The contained error is accessible via the `.err` property, either directly or after narrowing with `isErr()`.
|
||||||
|
*/
|
||||||
|
export class Err<E> implements ResultInterface<any, E> {
|
||||||
|
constructor(readonly err: E) {}
|
||||||
|
|
||||||
|
isOk(): this is never {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isErr(): this is Err<E> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
map<U>(): Err<E> {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapErr<F>(fn: (error: E) => F): Err<F> {
|
||||||
|
return err(fn(this.err));
|
||||||
|
}
|
||||||
|
|
||||||
|
flatMap(): this {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
flatMapErr<R extends Result<any, any>>(fn: (error: E) => R): R {
|
||||||
|
return fn(this.err);
|
||||||
|
}
|
||||||
|
|
||||||
|
match<U>(onOk: (value: never) => U, onErr: (error: E) => U): U {
|
||||||
|
return onErr(this.err);
|
||||||
|
}
|
||||||
|
|
||||||
|
filter(): Err<E> {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
toNullable(): null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
toUndefined(): undefined {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `Err(${this.err})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a result that is either Ok containing a value or Err containing an error.
|
||||||
|
* @remarks Use `isOk()` or `isErr()` to check the result type and access the contained value via `.value` or error via `.err`.
|
||||||
|
* ```ts
|
||||||
|
* const result = ok(42);
|
||||||
|
* if (result.isOk()) {
|
||||||
|
* console.log(result.value); // 42
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const result = err('failed');
|
||||||
|
* if (result.isErr()) {
|
||||||
|
* console.log(result.err); // 'failed'
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export type Result<T, E> = Ok<T> | Err<E>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a promise that resolves to a Result.
|
||||||
|
*/
|
||||||
|
export type ResultPromise<T, E> = Promise<Result<T, E>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a promise that resolves to Ok.
|
||||||
|
*/
|
||||||
|
export type OkPromise<T> = Promise<Ok<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a promise that resolves to Err.
|
||||||
|
*/
|
||||||
|
export type ErrPromise<E> = Promise<Err<E>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the result of partitioning an array of results into values and errors.
|
||||||
|
*/
|
||||||
|
export type ResultPartitionResult<T, E> = { values: T[]; errors: E[] };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a successful result containing the specified value.
|
||||||
|
* @param value The value to wrap in a result.
|
||||||
|
* @returns An Ok containing the value.
|
||||||
|
* ```ts
|
||||||
|
* const result = ok(42);
|
||||||
|
* if (result.isOk()) {
|
||||||
|
* console.log(result.value); // 42
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const ok = <T>(value: T): Ok<T> => new Ok(value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an error result containing the specified error.
|
||||||
|
* @param error The error to wrap in a result.
|
||||||
|
* @returns An Err containing the error.
|
||||||
|
* ```ts
|
||||||
|
* const result = err('failed');
|
||||||
|
* if (result.isErr()) {
|
||||||
|
* console.log(result.err); // 'failed'
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const err = <E>(error: E): Err<E> => new Err(error);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines an array of results into a single result containing an array of values.
|
||||||
|
* @param results The array of results to combine.
|
||||||
|
* @returns Ok containing an array of all values if all results are Ok, the first Err otherwise.
|
||||||
|
* @remarks Short-circuits on the first Err encountered — remaining results are not evaluated. The returned Err is the original instance by reference. When all inputs are known `Ok`, the return type narrows to `Ok<T[]>`.
|
||||||
|
* ```ts
|
||||||
|
* Result.all([ok(1), ok(2), ok(3)]) // Ok([1, 2, 3])
|
||||||
|
* Result.all([ok(1), err('error'), ok(3)]) // Err('error')
|
||||||
|
* Result.all([]) // Ok([])
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
function resultAll<T>(results: Ok<T>[]): Ok<T[]>;
|
||||||
|
function resultAll<T, E>(results: Result<T, E>[]): Result<T[], E>;
|
||||||
|
function resultAll<T, E>(results: Result<T, E>[]): Result<T[], E> {
|
||||||
|
const values = new Array<T>(results.length);
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
const result = results[i];
|
||||||
|
if (result.isErr()) return result;
|
||||||
|
values[i] = result.value;
|
||||||
|
}
|
||||||
|
return ok(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines an array of result promises into a single promise of a result containing an array of values.
|
||||||
|
* @param promises The array of result promises to combine.
|
||||||
|
* @returns A promise that resolves to Ok containing an array of all values if all results are Ok, the first Err otherwise.
|
||||||
|
* @remarks Rejects if any input promise rejects. When all inputs are known `Promise<Ok>`, the return type narrows to `Promise<Ok<T[]>>`.
|
||||||
|
* ```ts
|
||||||
|
* await ResultPromise.all([Promise.resolve(ok(1)), Promise.resolve(ok(2))]) // Ok([1, 2])
|
||||||
|
* await ResultPromise.all([Promise.resolve(ok(1)), Promise.resolve(err('error'))]) // Err('error')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async function resultPromiseAll<T>(promises: Promise<Ok<T>>[]): Promise<Ok<T[]>>;
|
||||||
|
async function resultPromiseAll<T, E>(promises: Promise<Result<T, E>>[]): Promise<Result<T[], E>>;
|
||||||
|
async function resultPromiseAll<T, E>(promises: Promise<Result<T, E>>[]): Promise<Result<T[], E>> {
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
return resultAll(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal Type-level guard: use `ResultPromise.from()` for async functions.
|
||||||
|
*/
|
||||||
|
function resultFrom(
|
||||||
|
fn: () => PromiseLike<any>,
|
||||||
|
errorMap?: (error: unknown) => any,
|
||||||
|
): 'ERROR: Result.from() is synchronous — use ResultPromise.from() for async functions';
|
||||||
|
/**
|
||||||
|
* Creates a result by executing a function and catching any thrown errors.
|
||||||
|
* @param fn Function to execute.
|
||||||
|
* @param errorMap Optional function to transform caught errors. When omitted, `E` defaults to `unknown` and the caught error is cast unsafely — prefer always providing `errorMap` for type safety.
|
||||||
|
* @returns Ok containing the function's return value, or Err containing the caught (or mapped) error.
|
||||||
|
* @remarks Unlike `Option.from`, there is no value overload — raw values don't have
|
||||||
|
* a natural Ok/Err mapping. Use `ok(value)` or `err(error)` directly to wrap known values.
|
||||||
|
* ```ts
|
||||||
|
* Result.from(() => JSON.parse('{"valid": true}')) // Ok({valid: true})
|
||||||
|
* Result.from(() => JSON.parse('invalid')) // Err(SyntaxError) — typed as Result<any, unknown>
|
||||||
|
* Result.from(() => JSON.parse('invalid'), e => 'Parse failed') // Err('Parse failed') — typed as Result<any, string>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
function resultFrom<T, E = unknown>(fn: () => T, errorMap?: (error: unknown) => E): Result<T, E>;
|
||||||
|
function resultFrom<T, E = unknown>(fn: () => T, errorMap?: (error: unknown) => E): any {
|
||||||
|
try {
|
||||||
|
return ok(fn());
|
||||||
|
} catch (error) {
|
||||||
|
return err(errorMap ? errorMap(error) : (error as E));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace providing static utility methods for working with collections of results.
|
||||||
|
*/
|
||||||
|
export const Result = {
|
||||||
|
from: resultFrom,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a value is a Result.
|
||||||
|
* @param value The value to test.
|
||||||
|
* @returns `true` if the value is a Result, `false` otherwise.
|
||||||
|
* @remarks This is a runtime `instanceof` check — it narrows to `Result<unknown, unknown>` by default. The generic parameters `T` and `E` can be specified explicitly (e.g., `Result.isResult<number, string>(x)`) but are caller-asserted, not runtime-validated.
|
||||||
|
* ```ts
|
||||||
|
* Result.isResult(ok(42)) // true
|
||||||
|
* Result.isResult(err('error')) // true
|
||||||
|
* Result.isResult(42) // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
isResult<T, E>(value: unknown): value is Result<T, E> {
|
||||||
|
return value instanceof Ok || value instanceof Err;
|
||||||
|
},
|
||||||
|
|
||||||
|
all: resultAll,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Partitions an array of results into successful values and errors.
|
||||||
|
* @param results The array of results to partition.
|
||||||
|
* @returns An object containing arrays of extracted values and errors.
|
||||||
|
* ```ts
|
||||||
|
* Result.partition([ok(1), err('error'), ok(3)]) // { values: [1, 3], errors: ['error'] }
|
||||||
|
* Result.partition([ok(1), ok(2)]) // { values: [1, 2], errors: [] }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
partition<T, E>(results: Result<T, E>[]): ResultPartitionResult<T, E> {
|
||||||
|
const values: T[] = [];
|
||||||
|
const errors: E[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
const result = results[i];
|
||||||
|
if (result.isOk()) {
|
||||||
|
values.push(result.value);
|
||||||
|
} else {
|
||||||
|
errors.push(result.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { values, errors };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts all values from Ok instances in an array of results.
|
||||||
|
* @param results The array of results to compact.
|
||||||
|
* @returns An array containing only the values from Ok instances.
|
||||||
|
* ```ts
|
||||||
|
* Result.compact([ok(1), err('error'), ok(3)]) // [1, 3]
|
||||||
|
* Result.compact([err('error1'), err('error2')]) // []
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
compact<T, E>(results: Result<T, E>[]): T[] {
|
||||||
|
const values: T[] = [];
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
const result = results[i];
|
||||||
|
if (result.isOk()) values.push(result.value);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether any result in an array is successful.
|
||||||
|
* @param results The array of results to test.
|
||||||
|
* @returns `true` if at least one result is Ok, `false` otherwise.
|
||||||
|
* ```ts
|
||||||
|
* Result.some([ok(1), err('error'), ok(3)]) // true
|
||||||
|
* Result.some([err('error1'), err('error2')]) // false
|
||||||
|
* Result.some([]) // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
some<T, E>(results: Result<T, E>[]): boolean {
|
||||||
|
return results.some((result) => result.isOk());
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether every result in an array is successful.
|
||||||
|
* @param results The array of results to test.
|
||||||
|
* @returns `true` if all results are Ok, `false` otherwise.
|
||||||
|
* ```ts
|
||||||
|
* Result.every([ok(1), ok(2)]) // true
|
||||||
|
* Result.every([ok(1), err('error')]) // false
|
||||||
|
* Result.every([]) // true
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
every<T, E>(results: Result<T, E>[]): boolean {
|
||||||
|
return results.every((result) => result.isOk());
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace providing async utility methods for working with collections of result promises.
|
||||||
|
*/
|
||||||
|
export const ResultPromise = {
|
||||||
|
/**
|
||||||
|
* Creates a result promise by executing a function and catching any thrown errors.
|
||||||
|
* @param fn Function to execute.
|
||||||
|
* @param errorMap Optional function to transform caught errors.
|
||||||
|
* @returns A promise that resolves to Ok containing the function's return value, or Err containing the caught error.
|
||||||
|
* ```ts
|
||||||
|
* await ResultPromise.from(() => fetch('/api').then(r => r.json())) // Ok({data: ...}) or Err(TypeError) if fetch fails
|
||||||
|
* await ResultPromise.from(() => Promise.reject(new Error('failed'))) // Err(Error('failed')) — typed as Result<never, unknown>
|
||||||
|
* await ResultPromise.from(() => Promise.reject(new Error('failed')), () => 'Request failed') // Err('Request failed') — typed as Result<never, string>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async from<T, E = unknown>(fn: () => Promise<T>, errorMap?: (error: unknown) => E): ResultPromise<T, E> {
|
||||||
|
try {
|
||||||
|
const value = await fn();
|
||||||
|
return ok(value);
|
||||||
|
} catch (error) {
|
||||||
|
return err(errorMap ? errorMap(error) : (error as E));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
all: resultPromiseAll,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Partitions an array of result promises into successful values and errors.
|
||||||
|
* @param promises The array of result promises to partition.
|
||||||
|
* @returns A promise that resolves to an object containing arrays of extracted values and errors.
|
||||||
|
* @remarks Rejects if any input promise rejects.
|
||||||
|
* ```ts
|
||||||
|
* await ResultPromise.partition([Promise.resolve(ok(1)), Promise.resolve(err('error'))]) // { values: [1], errors: ['error'] }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async partition<T, E>(promises: Promise<Result<T, E>>[]): Promise<ResultPartitionResult<T, E>> {
|
||||||
|
const resolved = await Promise.all(promises);
|
||||||
|
return Result.partition(resolved);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts all values from Ok instances in an array of result promises.
|
||||||
|
* @param promises The array of result promises to compact.
|
||||||
|
* @returns A promise that resolves to an array containing only the values from Ok instances.
|
||||||
|
* @remarks Rejects if any input promise rejects.
|
||||||
|
* ```ts
|
||||||
|
* await ResultPromise.compact([Promise.resolve(ok(1)), Promise.resolve(err('error'))]) // [1]
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async compact<T, E>(promises: Promise<Result<T, E>>[]): Promise<T[]> {
|
||||||
|
const resolved = await Promise.all(promises);
|
||||||
|
return Result.compact(resolved);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether any result promise in an array resolves to a successful result.
|
||||||
|
* @param promises The array of result promises to test.
|
||||||
|
* @returns A promise that resolves to `true` if at least one result is Ok, `false` otherwise.
|
||||||
|
* @remarks Rejects if any input promise rejects.
|
||||||
|
* ```ts
|
||||||
|
* await ResultPromise.some([Promise.resolve(ok(1)), Promise.resolve(err('error'))]) // true
|
||||||
|
* await ResultPromise.some([Promise.resolve(err('error1')), Promise.resolve(err('error2'))]) // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async some<T, E>(promises: Promise<Result<T, E>>[]): Promise<boolean> {
|
||||||
|
const resolved = await Promise.all(promises);
|
||||||
|
return Result.some(resolved);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether every result promise in an array resolves to a successful result.
|
||||||
|
* @param promises The array of result promises to test.
|
||||||
|
* @returns A promise that resolves to `true` if all results are Ok, `false` otherwise.
|
||||||
|
* @remarks Rejects if any input promise rejects.
|
||||||
|
* ```ts
|
||||||
|
* await ResultPromise.every([Promise.resolve(ok(1)), Promise.resolve(ok(2))]) // true
|
||||||
|
* await ResultPromise.every([Promise.resolve(ok(1)), Promise.resolve(err('error'))]) // false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async every<T, E>(promises: Promise<Result<T, E>>[]): Promise<boolean> {
|
||||||
|
const resolved = await Promise.all(promises);
|
||||||
|
return Result.every(resolved);
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
335
src/result/spec/filter.spec.ts
Normal file
335
src/result/spec/filter.spec.ts
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Result, ResultInterface, Ok, Err, ok, err } from '../';
|
||||||
|
|
||||||
|
// Helper to use the interface signatures which correctly unify T and E
|
||||||
|
const ri = <T, E>(r: Result<T, E>): ResultInterface<T, E> => r;
|
||||||
|
|
||||||
|
describe('Result.filter - Runtime Tests', () => {
|
||||||
|
describe('filter with Ok', () => {
|
||||||
|
it('should return Ok when predicate returns true', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult: ResultInterface<number, string> = ok(42);
|
||||||
|
const predicate = vi.fn((x: number) => x > 0);
|
||||||
|
const errorFactory = vi.fn((x: number) => `${x} is not positive`);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = okResult.filter(predicate, errorFactory);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate).toHaveBeenCalledWith(42);
|
||||||
|
expect(predicate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(errorFactory).not.toHaveBeenCalled();
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Err with errorFactory result when predicate returns false', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult: ResultInterface<number, string> = ok(42);
|
||||||
|
const predicate = vi.fn((x: number) => x < 0);
|
||||||
|
const errorFactory = vi.fn((x: number) => `${x} is not negative`);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = okResult.filter(predicate, errorFactory);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate).toHaveBeenCalledWith(42);
|
||||||
|
expect(predicate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(errorFactory).toHaveBeenCalledWith(42);
|
||||||
|
expect(errorFactory).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('42 is not negative');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with various predicate conditions', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
{ value: 5, predicate: (x: number) => x > 3, errorFactory: (x: number) => `${x} <= 3`, expected: true },
|
||||||
|
{ value: 2, predicate: (x: number) => x > 3, errorFactory: (x: number) => `${x} <= 3`, expected: false },
|
||||||
|
{ value: 'hello', predicate: (x: string) => x.length > 3, errorFactory: (x: string) => `"${x}" too short`, expected: true },
|
||||||
|
{ value: 'hi', predicate: (x: string) => x.length > 3, errorFactory: (x: string) => `"${x}" too short`, expected: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ value, predicate, errorFactory, expected }) => {
|
||||||
|
// Act
|
||||||
|
const result = ri(ok(value) as Result<typeof value, string>).filter(
|
||||||
|
predicate as (v: typeof value) => boolean,
|
||||||
|
errorFactory as (v: typeof value) => string
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if (expected) {
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value).toBe(value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex object filtering', () => {
|
||||||
|
// Arrange
|
||||||
|
const users = [
|
||||||
|
{ name: 'Alice', age: 25, active: true },
|
||||||
|
{ name: 'Bob', age: 17, active: true },
|
||||||
|
{ name: 'Charlie', age: 30, active: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
users.forEach((user) => {
|
||||||
|
// Act
|
||||||
|
const activeAdult = ri(ok(user) as Result<typeof user, string>).filter(
|
||||||
|
(u) => u.active && u.age >= 18,
|
||||||
|
(u) => `${u.name} is not an active adult`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if (user.name === 'Alice') {
|
||||||
|
expect(activeAdult.isOk()).toBe(true);
|
||||||
|
if (activeAdult.isOk()) {
|
||||||
|
expect(activeAdult.value).toBe(user);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expect(activeAdult.isErr()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle type predicate filtering', () => {
|
||||||
|
// Arrange
|
||||||
|
const mixedValues: (string | number)[] = [42, 'hello', 100, 'world'];
|
||||||
|
|
||||||
|
mixedValues.forEach((value) => {
|
||||||
|
// Arrange
|
||||||
|
const result: ResultInterface<string | number, string> = ok(value);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberResult = result.filter(
|
||||||
|
(x): x is number => typeof x === 'number',
|
||||||
|
(x) => `${x} is not a number`
|
||||||
|
);
|
||||||
|
const stringResult = result.filter(
|
||||||
|
(x): x is string => typeof x === 'string',
|
||||||
|
(x) => `${x} is not a string`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
expect(numberResult.isOk()).toBe(true);
|
||||||
|
expect(stringResult.isErr()).toBe(true);
|
||||||
|
if (numberResult.isOk()) {
|
||||||
|
expect(numberResult.value).toBe(value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expect(numberResult.isErr()).toBe(true);
|
||||||
|
expect(stringResult.isOk()).toBe(true);
|
||||||
|
if (stringResult.isOk()) {
|
||||||
|
expect(stringResult.value).toBe(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null and undefined values correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const nullResult: ResultInterface<null, string> = ok(null);
|
||||||
|
const undefinedResult: ResultInterface<undefined, string> = ok(undefined);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const nullFiltered = nullResult.filter((x) => x === null, () => 'not null');
|
||||||
|
const undefinedFiltered = undefinedResult.filter((x) => x === undefined, () => 'not undefined');
|
||||||
|
const nullRejected = nullResult.filter((x) => x !== null, () => 'was null');
|
||||||
|
const undefinedRejected = undefinedResult.filter((x) => x !== undefined, () => 'was undefined');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(nullFiltered.isOk()).toBe(true);
|
||||||
|
expect(undefinedFiltered.isOk()).toBe(true);
|
||||||
|
expect(nullRejected.isErr()).toBe(true);
|
||||||
|
expect(undefinedRejected.isErr()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter with Err', () => {
|
||||||
|
it('should return Err without calling predicate or errorFactory', () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult: ResultInterface<number, string> = err('original error');
|
||||||
|
const predicate = vi.fn((x: number) => x > 0);
|
||||||
|
const errorFactory = vi.fn((x: number) => `${x} failed`);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = errResult.filter(predicate, errorFactory);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate).not.toHaveBeenCalled();
|
||||||
|
expect(errorFactory).not.toHaveBeenCalled();
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('original error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Err for any predicate', () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult: ResultInterface<string, string> = err('error');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result1 = errResult.filter((x) => x.length > 0, () => 'empty');
|
||||||
|
const result2 = errResult.filter((x) => x.length === 0, () => 'not empty');
|
||||||
|
const result3 = errResult.filter((x): x is string => typeof x === 'string', () => 'not string');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result1.isErr()).toBe(true);
|
||||||
|
expect(result2.isErr()).toBe(true);
|
||||||
|
expect(result3.isErr()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter chaining', () => {
|
||||||
|
it('should allow chaining multiple filters', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult: ResultInterface<number, string> = ok(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = ri(okResult
|
||||||
|
.filter((x) => x > 0, (x) => `${x} is not positive`))
|
||||||
|
.filter((x) => x < 100, (x) => `${x} is too large`);
|
||||||
|
const finalResult = ri(result)
|
||||||
|
.filter((x) => x % 2 === 0, (x) => `${x} is not even`);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(finalResult.isOk()).toBe(true);
|
||||||
|
if (finalResult.isOk()) {
|
||||||
|
expect(finalResult.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should short-circuit on first failed filter', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult: ResultInterface<number, string> = ok(42);
|
||||||
|
const predicate1 = vi.fn((x: number) => x > 0);
|
||||||
|
const predicate2 = vi.fn((x: number) => x < 10); // This should fail
|
||||||
|
const predicate3 = vi.fn((x: number) => x % 2 === 0); // This should not be called
|
||||||
|
const errorFactory1 = vi.fn(() => 'not positive');
|
||||||
|
const errorFactory2 = vi.fn((x: number) => `${x} >= 10`);
|
||||||
|
const errorFactory3 = vi.fn(() => 'not even');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const step1 = ri(okResult.filter(predicate1, errorFactory1));
|
||||||
|
const step2 = ri(step1.filter(predicate2, errorFactory2));
|
||||||
|
const result = step2.filter(predicate3, errorFactory3);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate1).toHaveBeenCalledWith(42);
|
||||||
|
expect(predicate2).toHaveBeenCalledWith(42);
|
||||||
|
expect(predicate3).not.toHaveBeenCalled();
|
||||||
|
expect(errorFactory1).not.toHaveBeenCalled();
|
||||||
|
expect(errorFactory2).toHaveBeenCalledWith(42);
|
||||||
|
expect(errorFactory3).not.toHaveBeenCalled();
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('42 >= 10');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter error handling', () => {
|
||||||
|
it('should propagate errors from predicate function', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult: ResultInterface<number, string> = ok(42);
|
||||||
|
const error = new Error('Predicate error');
|
||||||
|
const predicate = vi.fn(() => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(() => {
|
||||||
|
okResult.filter(predicate, () => 'error');
|
||||||
|
}).toThrow('Predicate error');
|
||||||
|
|
||||||
|
expect(predicate).toHaveBeenCalledWith(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should propagate errors from errorFactory function', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult: ResultInterface<number, string> = ok(42);
|
||||||
|
const error = new Error('ErrorFactory error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(() => {
|
||||||
|
okResult.filter(() => false, () => { throw error; });
|
||||||
|
}).toThrow('ErrorFactory error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call predicate or errorFactory on Err even if they would throw', () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult: ResultInterface<number, string> = err('error');
|
||||||
|
const predicate = vi.fn(() => {
|
||||||
|
throw new Error('Should not be called');
|
||||||
|
});
|
||||||
|
const errorFactory = vi.fn(() => {
|
||||||
|
throw new Error('Should not be called');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = errResult.filter(predicate, errorFactory);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(predicate).not.toHaveBeenCalled();
|
||||||
|
expect(errorFactory).not.toHaveBeenCalled();
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter with edge cases', () => {
|
||||||
|
it('should handle falsy values correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const falsyValues = [0, false, '', NaN];
|
||||||
|
|
||||||
|
falsyValues.forEach((value) => {
|
||||||
|
// Arrange
|
||||||
|
const result: ResultInterface<typeof value, string> = ok(value);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const truthyResult = result.filter((x) => !!x, () => 'falsy');
|
||||||
|
const falsyResult = result.filter((x) => !x, () => 'truthy');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(truthyResult.isErr()).toBe(true);
|
||||||
|
expect(falsyResult.isOk()).toBe(true);
|
||||||
|
if (falsyResult.isOk()) {
|
||||||
|
expect(falsyResult.value).toBe(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex nested structures', () => {
|
||||||
|
// Arrange
|
||||||
|
const complexObject = {
|
||||||
|
data: {
|
||||||
|
items: [
|
||||||
|
{ id: 1, name: 'Item 1', tags: ['tag1', 'tag2'] },
|
||||||
|
{ id: 2, name: 'Item 2', tags: ['tag3'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result: ResultInterface<typeof complexObject, string> = ok(complexObject);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filtered = result.filter(
|
||||||
|
(obj) => obj.data.items.length > 0 && obj.data.items.every((item) => item.tags.length > 0),
|
||||||
|
() => 'validation failed'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filtered.isOk()).toBe(true);
|
||||||
|
if (filtered.isOk()) {
|
||||||
|
expect(filtered.value).toBe(complexObject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
236
src/result/spec/filter.type-spec.ts
Normal file
236
src/result/spec/filter.type-spec.ts
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Result, ResultInterface, Ok, Err, ok, err } from "../";
|
||||||
|
|
||||||
|
// Helper to use the interface signatures which correctly unify T and E
|
||||||
|
const ri = <T, E>(r: Result<T, E>) => r as unknown as ResultInterface<T, E>;
|
||||||
|
|
||||||
|
describe("Result.filter - Type Tests", () => {
|
||||||
|
describe("filter directly on Result<T, E> union", () => {
|
||||||
|
it("should accept filter with errorFactory without ri() helper", () => {
|
||||||
|
const result: Result<number, string> = ok(42) as Result<number, string>;
|
||||||
|
const filtered = result.filter(x => x > 0, () => "not positive");
|
||||||
|
expectTypeOf(filtered).toEqualTypeOf<Ok<number> | Err<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with type predicate directly on union", () => {
|
||||||
|
const result: Result<string | number, string> = ok(42) as Result<string | number, string>;
|
||||||
|
const filtered = result.filter(
|
||||||
|
(x): x is number => typeof x === "number",
|
||||||
|
() => "not a number"
|
||||||
|
);
|
||||||
|
expectTypeOf(filtered).toEqualTypeOf<Ok<number> | Err<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Ok<T> | Err<F> on known Ok with generic errorFactory", () => {
|
||||||
|
const filtered = ok(42).filter(x => x > 0, x => `${x} is too small`);
|
||||||
|
expectTypeOf(filtered).toEqualTypeOf<Ok<number> | Err<string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter with type predicate", () => {
|
||||||
|
it("should narrow type with type predicate", () => {
|
||||||
|
// Arrange
|
||||||
|
const unionResult = ri<string | number, string>(ok(42) as Result<string | number, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberResult = unionResult.filter(
|
||||||
|
(x): x is number => typeof x === "number",
|
||||||
|
(x) => `${x} is not a number`
|
||||||
|
);
|
||||||
|
const stringResult = unionResult.filter(
|
||||||
|
(x): x is string => typeof x === "string",
|
||||||
|
(x) => `${x} is not a string`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberResult).toEqualTypeOf<Result<number, string>>();
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<Result<string, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex type predicates", () => {
|
||||||
|
// Arrange
|
||||||
|
interface User { type: "user"; name: string; }
|
||||||
|
interface Admin { type: "admin"; name: string; permissions: string[]; }
|
||||||
|
type Person = User | Admin;
|
||||||
|
|
||||||
|
const personResult = ri<Person, string>(ok({ type: "user", name: "John" }) as Result<Person, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const adminResult = personResult.filter(
|
||||||
|
(p): p is Admin => p.type === "admin",
|
||||||
|
(p) => `${p.name} is not an admin`
|
||||||
|
);
|
||||||
|
const userResult = personResult.filter(
|
||||||
|
(p): p is User => p.type === "user",
|
||||||
|
(p) => `${p.name} is not a user`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(adminResult).toEqualTypeOf<Result<Admin, string>>();
|
||||||
|
expectTypeOf(userResult).toEqualTypeOf<Result<User, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle nullable type predicates", () => {
|
||||||
|
// Arrange
|
||||||
|
const nullableResult = ri<string | null, string>(ok("hello") as Result<string | null, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const nonNullResult = nullableResult.filter(
|
||||||
|
(x): x is string => x !== null,
|
||||||
|
() => "was null"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(nonNullResult).toEqualTypeOf<Result<string, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle array type predicates", () => {
|
||||||
|
// Arrange
|
||||||
|
const arrayResult = ri<unknown[], string>(ok([1, 2, 3]) as Result<unknown[], string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberArrayResult = arrayResult.filter(
|
||||||
|
(arr): arr is number[] => arr.every(item => typeof item === "number"),
|
||||||
|
() => "not all numbers"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberArrayResult).toEqualTypeOf<Result<number[], string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter with boolean predicate", () => {
|
||||||
|
it("should preserve original type with boolean predicate", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberResult = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
const stringResult = ri<string, string>(ok("hello") as Result<string, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filteredNumber = numberResult.filter(x => x > 0, () => "not positive");
|
||||||
|
const filteredString = stringResult.filter(x => x.length > 0, () => "empty");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(filteredNumber).toEqualTypeOf<Result<number, string>>();
|
||||||
|
expectTypeOf(filteredString).toEqualTypeOf<Result<string, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with complex boolean predicates", () => {
|
||||||
|
// Arrange
|
||||||
|
const objectResult = ri<{ id: number; name: string; active: boolean }, string>(
|
||||||
|
ok({ id: 1, name: "test", active: true }) as Result<{ id: number; name: string; active: boolean }, string>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filteredObject = objectResult.filter(
|
||||||
|
obj => obj.active && obj.name.length > 0,
|
||||||
|
(obj) => `${obj.name} is invalid`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(filteredObject).toEqualTypeOf<Result<{ id: number; name: string; active: boolean }, string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter with Err", () => {
|
||||||
|
it("should preserve Err type regardless of predicate", () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult = ri<number, string>(err("error") as Result<number, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filteredWithBoolean = errResult.filter(x => x > 0, () => "not positive");
|
||||||
|
const filteredWithTypePredicate = errResult.filter(
|
||||||
|
(x): x is number => typeof x === "number",
|
||||||
|
() => "not a number"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(filteredWithBoolean).toEqualTypeOf<Result<number, string>>();
|
||||||
|
expectTypeOf(filteredWithTypePredicate).toEqualTypeOf<Result<number, string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter function signature validation", () => {
|
||||||
|
it("should be callable with correct predicate and errorFactory signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
const booleanPredicate = (x: number) => x > 0;
|
||||||
|
const typePredicate = (x: number): x is number => typeof x === "number";
|
||||||
|
const errorFactory = (x: number) => `${x} failed validation`;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result.filter).toBeCallableWith(booleanPredicate, errorFactory);
|
||||||
|
expectTypeOf(result.filter).toBeCallableWith(typePredicate, errorFactory);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be callable with incorrect predicate signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// @ts-expect-error - wrong parameter type for predicate
|
||||||
|
result.filter((x: string) => x.length > 0, () => "error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be callable without errorFactory", () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// @ts-expect-error - missing errorFactory argument
|
||||||
|
result.filter((x: number) => x > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be callable without any arguments", () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// @ts-expect-error - missing all arguments
|
||||||
|
result.filter();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter overload resolution", () => {
|
||||||
|
it("should resolve to type predicate overload when type predicate is provided", () => {
|
||||||
|
// Arrange
|
||||||
|
const unionResult = ri<string | number, string>(ok(42) as Result<string | number, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = unionResult.filter(
|
||||||
|
(x): x is number => typeof x === "number",
|
||||||
|
(x) => `${x} is not a number`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert - should be Result<number, string>, not Result<string | number, string>
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Result<number, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should resolve to boolean predicate overload when boolean predicate is provided", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberResult = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = numberResult.filter(x => x > 0, () => "not positive");
|
||||||
|
|
||||||
|
// Assert - should preserve original type
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Result<number, string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter with generic constraints", () => {
|
||||||
|
it("should work with generic type constraints", () => {
|
||||||
|
// Arrange
|
||||||
|
interface Lengthable { length: number; }
|
||||||
|
const lengthableResult = ri<Lengthable, string>(ok("hello") as Result<Lengthable, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = lengthableResult.filter(
|
||||||
|
(x): x is string => typeof x === "string",
|
||||||
|
() => "not a string"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Result<string, string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
330
src/result/spec/flatMap.spec.ts
Normal file
330
src/result/spec/flatMap.spec.ts
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Result, ResultInterface, Ok, Err, ok, err } from '../';
|
||||||
|
|
||||||
|
// Helper to use the interface signatures which correctly unify T and E
|
||||||
|
const ri = <T, E>(r: Result<T, E>): ResultInterface<T, E> => r;
|
||||||
|
|
||||||
|
describe('Result flatMap and flatMapErr - Runtime Tests', () => {
|
||||||
|
describe('flatMap method', () => {
|
||||||
|
describe('when Result is Ok', () => {
|
||||||
|
it('should apply function and flatten the result when function returns Ok', () => {
|
||||||
|
// Arrange
|
||||||
|
const result: ResultInterface<number, string> = ok(5);
|
||||||
|
const flatMapper = (x: number): Result<number, string> => ok(x * 2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMapped = result.flatMap(flatMapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(flatMapped.isOk()).toBe(true);
|
||||||
|
if (flatMapped.isOk()) {
|
||||||
|
expect(flatMapped.value).toBe(10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Err when function returns Err', () => {
|
||||||
|
// Arrange
|
||||||
|
const result: ResultInterface<number, string> = ok(5);
|
||||||
|
const flatMapper = (_x: number): Result<number, string> => err('failed');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMapped = result.flatMap(flatMapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(flatMapped.isErr()).toBe(true);
|
||||||
|
if (flatMapped.isErr()) {
|
||||||
|
expect(flatMapped.err).toBe('failed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle conditional logic in flatMapper', () => {
|
||||||
|
// Arrange
|
||||||
|
const positiveResult: ResultInterface<number, string> = ok(5);
|
||||||
|
const negativeResult: ResultInterface<number, string> = ok(-3);
|
||||||
|
const flatMapper = (x: number): Result<number, string> =>
|
||||||
|
x > 0 ? ok(x * 2) : err('negative value');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const positiveOutput = positiveResult.flatMap(flatMapper);
|
||||||
|
const negativeOutput = negativeResult.flatMap(flatMapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(positiveOutput.isOk()).toBe(true);
|
||||||
|
expect(negativeOutput.isErr()).toBe(true);
|
||||||
|
if (positiveOutput.isOk()) {
|
||||||
|
expect(positiveOutput.value).toBe(10);
|
||||||
|
}
|
||||||
|
if (negativeOutput.isErr()) {
|
||||||
|
expect(negativeOutput.err).toBe('negative value');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle type transformations', () => {
|
||||||
|
// Arrange
|
||||||
|
const stringResult: ResultInterface<string, string> = ok('42');
|
||||||
|
const parseNumber = (str: string): Result<number, string> => {
|
||||||
|
const num = parseInt(str);
|
||||||
|
return isNaN(num) ? err('parse error') : ok(num);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const validOutput = stringResult.flatMap(parseNumber);
|
||||||
|
const invalidResult: ResultInterface<string, string> = ok('invalid');
|
||||||
|
const invalidOutput = invalidResult.flatMap(parseNumber);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(validOutput.isOk()).toBe(true);
|
||||||
|
expect(invalidOutput.isErr()).toBe(true);
|
||||||
|
if (validOutput.isOk()) {
|
||||||
|
expect(validOutput.value).toBe(42);
|
||||||
|
}
|
||||||
|
if (invalidOutput.isErr()) {
|
||||||
|
expect(invalidOutput.err).toBe('parse error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with complex business logic', () => {
|
||||||
|
// Arrange
|
||||||
|
type User = { id: number; email: string; verified: boolean };
|
||||||
|
type AuthToken = { token: string; userId: number };
|
||||||
|
|
||||||
|
const user: User = { id: 1, email: 'test@example.com', verified: true };
|
||||||
|
const userResult: ResultInterface<User, string> = ok(user);
|
||||||
|
|
||||||
|
const generateToken = (u: User): Result<AuthToken, string> => {
|
||||||
|
return u.verified
|
||||||
|
? ok({ token: `token_${u.id}`, userId: u.id })
|
||||||
|
: err('User not verified');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const tokenOutput = userResult.flatMap(generateToken);
|
||||||
|
const unverifiedResult: ResultInterface<User, string> = ok({ ...user, verified: false });
|
||||||
|
const unverifiedOutput = unverifiedResult.flatMap(generateToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(tokenOutput.isOk()).toBe(true);
|
||||||
|
expect(unverifiedOutput.isErr()).toBe(true);
|
||||||
|
if (tokenOutput.isOk()) {
|
||||||
|
expect(tokenOutput.value).toEqual({ token: 'token_1', userId: 1 });
|
||||||
|
}
|
||||||
|
if (unverifiedOutput.isErr()) {
|
||||||
|
expect(unverifiedOutput.err).toBe('User not verified');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not mutate the original result', () => {
|
||||||
|
// Arrange
|
||||||
|
const original = ok({ value: 10 });
|
||||||
|
const originalValue = original.value;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMapped = original.flatMap(obj => ok({ ...obj, value: obj.value * 2 }));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(original.value).toBe(originalValue);
|
||||||
|
expect(original.value.value).toBe(10);
|
||||||
|
if (flatMapped.isOk()) {
|
||||||
|
expect(flatMapped.value.value).toBe(20);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Result is Err', () => {
|
||||||
|
it('should return Err without calling the flatMapper function', () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult: ResultInterface<number, string> = err('error');
|
||||||
|
const flatMapperSpy = vi.fn((x: number): Result<number, string> => ok(x * 2));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = errResult.flatMap(flatMapperSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
expect(flatMapperSpy).not.toHaveBeenCalled();
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve Err through multiple flatMap operations', () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult: ResultInterface<number, string> = err('original error');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const step1 = ri(errResult.flatMap((x): Result<number, string> => ok(x * 2)));
|
||||||
|
const step2 = ri(step1.flatMap((x): Result<string, string> => ok(x.toString())));
|
||||||
|
const result = step2.flatMap((s): Result<number, string> => ok(s.length));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('original error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('flatMapErr method', () => {
|
||||||
|
describe('when Result is Err', () => {
|
||||||
|
it('should apply function and flatten the result when function returns Err', () => {
|
||||||
|
// Arrange
|
||||||
|
const result = err('error');
|
||||||
|
const flatMapper = (e: string) => err(e.toUpperCase());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMapped = result.flatMapErr(flatMapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(flatMapped.isErr()).toBe(true);
|
||||||
|
if (flatMapped.isErr()) {
|
||||||
|
expect(flatMapped.err).toBe('ERROR');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Ok when function returns Ok (recovery)', () => {
|
||||||
|
// Arrange
|
||||||
|
const result: ResultInterface<string, string> = err('error');
|
||||||
|
const flatMapper = (_e: string): Result<string, string> => ok('recovered');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMapped = result.flatMapErr(flatMapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(flatMapped.isOk()).toBe(true);
|
||||||
|
if (flatMapped.isOk()) {
|
||||||
|
expect(flatMapped.value).toBe('recovered');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle conditional recovery logic', () => {
|
||||||
|
// Arrange
|
||||||
|
const recoverableErr: ResultInterface<number, string> = err('timeout');
|
||||||
|
const fatalErr: ResultInterface<number, string> = err('fatal');
|
||||||
|
const recover = (e: string): Result<number, string> =>
|
||||||
|
e === 'timeout' ? ok(0) : err(`unrecoverable: ${e}`);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const recovered = recoverableErr.flatMapErr(recover);
|
||||||
|
const notRecovered = fatalErr.flatMapErr(recover);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(recovered.isOk()).toBe(true);
|
||||||
|
expect(notRecovered.isErr()).toBe(true);
|
||||||
|
if (recovered.isOk()) expect(recovered.value).toBe(0);
|
||||||
|
if (notRecovered.isErr()) expect(notRecovered.err).toBe('unrecoverable: fatal');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error type transformations', () => {
|
||||||
|
// Arrange
|
||||||
|
const result = err('not found');
|
||||||
|
const transform = (e: string) => err({ code: 404, message: e });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const transformed = result.flatMapErr(transform);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(transformed.isErr()).toBe(true);
|
||||||
|
if (transformed.isErr()) {
|
||||||
|
expect(transformed.err).toEqual({ code: 404, message: 'not found' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Result is Ok', () => {
|
||||||
|
it('should return Ok without calling the flatMapper function', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult: ResultInterface<number, string> = ok(42);
|
||||||
|
const flatMapperSpy = vi.fn((e: string): Result<number, string> => err(e.toUpperCase()));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = okResult.flatMapErr(flatMapperSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
expect(flatMapperSpy).not.toHaveBeenCalled();
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve Ok through multiple flatMapErr operations', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult: ResultInterface<number, string> = ok(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const step1 = ri(okResult.flatMapErr((e): Result<number, string> => err(e.toUpperCase())));
|
||||||
|
const step2 = ri(step1.flatMapErr((e): Result<number, number> => err(e.length)));
|
||||||
|
const result = step2.flatMapErr((n): Result<number, string> => err(`Error length: ${n}`));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('flatMap and flatMapErr integration', () => {
|
||||||
|
it('should work together in complex chains', () => {
|
||||||
|
// Arrange
|
||||||
|
const parseAndValidate = (input: string): Result<number, string> => {
|
||||||
|
const num = parseInt(input);
|
||||||
|
if (isNaN(num)) return err('parse error');
|
||||||
|
if (num < 0) return err('negative');
|
||||||
|
return ok(num);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const successInput: ResultInterface<string, string> = ok('42');
|
||||||
|
const successResult = ri(successInput.flatMap(parseAndValidate))
|
||||||
|
.flatMapErr((e): Result<number, string> => err(`Validation failed: ${e}`));
|
||||||
|
|
||||||
|
const failInput: ResultInterface<string, string> = ok('invalid');
|
||||||
|
const failResult = ri(failInput.flatMap(parseAndValidate))
|
||||||
|
.flatMapErr((e): Result<number, string> => err(`Validation failed: ${e}`));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(successResult.isOk()).toBe(true);
|
||||||
|
if (successResult.isOk()) expect(successResult.value).toBe(42);
|
||||||
|
|
||||||
|
expect(failResult.isErr()).toBe(true);
|
||||||
|
if (failResult.isErr()) expect(failResult.err).toBe('Validation failed: parse error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should short-circuit on first Err in flatMap chain', () => {
|
||||||
|
// Arrange
|
||||||
|
const mapSpy = vi.fn((x: string): Result<string, string> => ok(x.toUpperCase()));
|
||||||
|
const flatMapSpy = vi.fn((x: string): Result<number, string> => ok(x.length));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const input: ResultInterface<string, string> = ok('test');
|
||||||
|
const step1 = ri(input.flatMap((_x): Result<string, string> => err('stop here')));
|
||||||
|
const step2 = ri(step1.flatMap(mapSpy));
|
||||||
|
const result = step2.flatMap(flatMapSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
expect(mapSpy).not.toHaveBeenCalled();
|
||||||
|
expect(flatMapSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle edge cases with falsy values', () => {
|
||||||
|
// Arrange
|
||||||
|
const emptyStringResult: ResultInterface<string, string> = ok('');
|
||||||
|
const zeroResult: ResultInterface<number, string> = ok(0);
|
||||||
|
const falseResult: ResultInterface<boolean, string> = ok(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringOutput = emptyStringResult.flatMap((s): Result<number, string> => ok(s.length));
|
||||||
|
const numberOutput = zeroResult.flatMap((n): Result<string, string> => ok(n.toString()));
|
||||||
|
const booleanOutput = falseResult.flatMap((b): Result<boolean, string> => ok(!b));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if (stringOutput.isOk()) expect(stringOutput.value).toBe(0);
|
||||||
|
if (numberOutput.isOk()) expect(numberOutput.value).toBe('0');
|
||||||
|
if (booleanOutput.isOk()) expect(booleanOutput.value).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
215
src/result/spec/flatMap.type-spec.ts
Normal file
215
src/result/spec/flatMap.type-spec.ts
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Result, ResultInterface, Ok, Err, ok, err } from "../";
|
||||||
|
|
||||||
|
// Helper to use the interface signatures which correctly unify T and E
|
||||||
|
const ri = <T, E>(r: Result<T, E>) => r as unknown as ResultInterface<T, E>;
|
||||||
|
|
||||||
|
describe("Result flatMap and flatMapErr - Type Tests", () => {
|
||||||
|
describe("flatMap directly on Result<T, E> union", () => {
|
||||||
|
it("should accept callbacks returning Result<U, E> without ri() helper", () => {
|
||||||
|
const result: Result<number, string> = ok(42) as Result<number, string>;
|
||||||
|
const flatMapped = result.flatMap(x => x > 0 ? ok(x * 2) : err("negative"));
|
||||||
|
expectTypeOf(flatMapped).toEqualTypeOf<Result<number, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should infer correct type when callback returns only Ok", () => {
|
||||||
|
const result: Result<number, string> = ok(42) as Result<number, string>;
|
||||||
|
const flatMapped = result.flatMap(x => ok(x.toString()));
|
||||||
|
expectTypeOf(flatMapped).toEqualTypeOf<Ok<string> | Err<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return exact callback type on known Ok via R pattern", () => {
|
||||||
|
const flatMapped = ok(42).flatMap(x => ok(x.toString()));
|
||||||
|
expectTypeOf(flatMapped).toEqualTypeOf<Ok<string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("flatMapErr directly on Result<T, E> union", () => {
|
||||||
|
it("should accept callbacks returning Result<T, F> without ri() helper", () => {
|
||||||
|
const result: Result<number, string> = err("fail") as Result<number, string>;
|
||||||
|
const flatMapped = result.flatMapErr(e => e === "retry" ? ok(0) : err(e.length));
|
||||||
|
expectTypeOf(flatMapped).toEqualTypeOf<Ok<number> | Ok<number> | Err<number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return exact callback type on known Err via R pattern", () => {
|
||||||
|
const flatMapped = err("fail").flatMapErr(e => err(e.length));
|
||||||
|
expectTypeOf(flatMapped).toEqualTypeOf<Err<number>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("flatMap method", () => {
|
||||||
|
it("should flatten Ok<T> with function returning Result<U, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberResult = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMappedOk = numberResult.flatMap(x => ok(x * 2));
|
||||||
|
const flatMappedErr = numberResult.flatMap(x => err("error") as Result<number, string>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(flatMappedOk).toEqualTypeOf<Result<number, string>>();
|
||||||
|
expectTypeOf(flatMappedErr).toEqualTypeOf<Result<number, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle Err input correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult = ri<number, string>(err("error") as Result<number, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMapped = errResult.flatMap(x => ok(x * 2) as Result<number, string>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(flatMapped).toEqualTypeOf<Result<number, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should transform types correctly with flatMap", () => {
|
||||||
|
// Arrange
|
||||||
|
const stringResult = ri<string, string>(ok("42") as Result<string, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const parsed = stringResult.flatMap(str => {
|
||||||
|
const num = parseInt(str);
|
||||||
|
return isNaN(num) ? err("parse error") : ok(num);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(parsed).toEqualTypeOf<Result<number, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex type transformations", () => {
|
||||||
|
// Arrange
|
||||||
|
type ParseResult = { value: number; valid: boolean };
|
||||||
|
const inputResult = ri<string, string>(ok("123") as Result<string, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = inputResult.flatMap((str): Result<ParseResult, string> => {
|
||||||
|
const num = parseInt(str);
|
||||||
|
return isNaN(num)
|
||||||
|
? err("invalid number")
|
||||||
|
: ok({ value: num, valid: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Result<ParseResult, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with chained flatMap operations", () => {
|
||||||
|
// Arrange
|
||||||
|
const initialResult = ri<number, string>(ok(5) as Result<number, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const chained = ri(initialResult
|
||||||
|
.flatMap(x => x > 0 ? ok(x * 2) : err("negative") as Result<number, string>))
|
||||||
|
.flatMap(x => x < 20 ? ok(x.toString()) : err("too large") as Result<string, string>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(chained).toEqualTypeOf<Result<string, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with correct flatMapper function signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberResult = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberResult.flatMap).toBeCallableWith((x: number) => ok(x * 2) as Result<number, string>);
|
||||||
|
expectTypeOf(numberResult.flatMap).toBeCallableWith((x: number) => err("error") as Result<number, string>);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("flatMapErr method", () => {
|
||||||
|
it("should flatten Err<E> with function returning Result<T, F>", () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult = ri<string, string>(err("error") as Result<string, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMappedErr = errResult.flatMapErr(e => err(e.toUpperCase()));
|
||||||
|
const flatMappedOk = errResult.flatMapErr(e => ok("recovered") as Result<string, number>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(flatMappedErr).toEqualTypeOf<Result<string, string>>();
|
||||||
|
expectTypeOf(flatMappedOk).toEqualTypeOf<Result<string, number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle Ok input correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMapped = okResult.flatMapErr(e => err(e.length) as Result<number, number>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(flatMapped).toEqualTypeOf<Result<number, number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle error type transformations", () => {
|
||||||
|
// Arrange
|
||||||
|
type ApiError = { code: number; message: string };
|
||||||
|
type DisplayError = { title: string };
|
||||||
|
const errResult = ri<number, ApiError>(err({ code: 404, message: "Not found" }) as Result<number, ApiError>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const transformed = errResult.flatMapErr((e): Result<number, DisplayError> =>
|
||||||
|
err({ title: `Error ${e.code}` })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(transformed).toEqualTypeOf<Result<number, DisplayError>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle recovery (Err -> Ok)", () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult = ri<number, string>(err("timeout") as Result<number, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const recovered = errResult.flatMapErr((_e): Result<number, string> => ok(0));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(recovered).toEqualTypeOf<Result<number, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with correct flatMapper function signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult = ri<number, string>(err("error") as Result<number, string>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(errResult.flatMapErr).toBeCallableWith((e: string) => err(e.length) as Result<number, number>);
|
||||||
|
expectTypeOf(errResult.flatMapErr).toBeCallableWith((e: string) => ok(42) as Result<number, number>);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("flatMap and flatMapErr interaction", () => {
|
||||||
|
it("should work together in chains with correct type inference", () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ri<number, string>(ok(5) as Result<number, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const chained = ri(result
|
||||||
|
.flatMap(x => x > 0 ? ok(x.toString()) : err("negative") as Result<string, string>))
|
||||||
|
.flatMapErr(e => err({ message: e }) as Result<string, { message: string }>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(chained).toEqualTypeOf<Result<string, { message: string }>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle mixed transformations correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
type Input = { value: string };
|
||||||
|
type Output = { parsed: number };
|
||||||
|
|
||||||
|
const result = ri<Input, string>(ok({ value: "42" }) as Result<Input, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const transformed = ri(result
|
||||||
|
.flatMap((input): Result<Output, string> => {
|
||||||
|
const num = parseInt(input.value);
|
||||||
|
return isNaN(num) ? err("parse error") : ok({ parsed: num });
|
||||||
|
}))
|
||||||
|
.flatMapErr((e): Result<Output, { code: number; message: string }> =>
|
||||||
|
err({ code: 400, message: e })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(transformed).toEqualTypeOf<Result<Output, { code: number; message: string }>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
330
src/result/spec/is.spec.ts
Normal file
330
src/result/spec/is.spec.ts
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Result, Ok, Err, ok, err } from '../';
|
||||||
|
|
||||||
|
describe('Result isOk/isErr - Runtime Tests', () => {
|
||||||
|
describe('isOk() method', () => {
|
||||||
|
it('should return true for Ok instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
ok(42),
|
||||||
|
ok('hello'),
|
||||||
|
ok(true),
|
||||||
|
ok({ id: 1 }),
|
||||||
|
ok([1, 2, 3]),
|
||||||
|
ok(null),
|
||||||
|
ok(undefined),
|
||||||
|
ok(0),
|
||||||
|
ok(false),
|
||||||
|
ok('')
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(result => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for Err instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult = err('error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(errResult.isOk()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable access to value property after type narrowing', () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<string, number> = ok('hello') as Result<string, number>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value).toBe('hello');
|
||||||
|
expect(typeof result.value).toBe('string');
|
||||||
|
} else {
|
||||||
|
fail('Expected result to be Ok');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with complex objects', () => {
|
||||||
|
// Arrange
|
||||||
|
const complexObject = {
|
||||||
|
id: 1,
|
||||||
|
name: 'test',
|
||||||
|
nested: { data: [1, 2, 3] },
|
||||||
|
fn: (x: number) => x * 2
|
||||||
|
};
|
||||||
|
const result: Result<typeof complexObject, string> = ok(complexObject) as Result<typeof complexObject, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value).toBe(complexObject);
|
||||||
|
expect(result.value.id).toBe(1);
|
||||||
|
expect(result.value.name).toBe('test');
|
||||||
|
expect(result.value.nested.data).toEqual([1, 2, 3]);
|
||||||
|
expect(result.value.fn(5)).toBe(10);
|
||||||
|
} else {
|
||||||
|
fail('Expected result to be Ok');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with falsy values that are still Ok', () => {
|
||||||
|
// Arrange
|
||||||
|
const falsy: Result<number | boolean | string | null | undefined, string>[] = [
|
||||||
|
ok(0) as Result<number, string>,
|
||||||
|
ok(false) as Result<boolean, string>,
|
||||||
|
ok('') as Result<string, string>,
|
||||||
|
ok(null) as Result<null, string>,
|
||||||
|
ok(undefined) as Result<undefined, string>
|
||||||
|
];
|
||||||
|
|
||||||
|
falsy.forEach(result => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value !== undefined || result.value === undefined).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isErr() method', () => {
|
||||||
|
it('should return true for Err instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult = err('error');
|
||||||
|
const errAsResult: Result<any, string> = err('error') as Result<any, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(errResult.isErr()).toBe(true);
|
||||||
|
expect(errAsResult.isErr()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for Ok instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
ok(42),
|
||||||
|
ok('hello'),
|
||||||
|
ok(true),
|
||||||
|
ok({ id: 1 }),
|
||||||
|
ok([1, 2, 3]),
|
||||||
|
ok(null),
|
||||||
|
ok(undefined),
|
||||||
|
ok(0),
|
||||||
|
ok(false),
|
||||||
|
ok('')
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(result => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(result.isErr()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable access to err property after type narrowing', () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<number, string> = err('something failed') as Result<number, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('something failed');
|
||||||
|
expect(typeof result.err).toBe('string');
|
||||||
|
} else {
|
||||||
|
fail('Expected result to be Err');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with different error types', () => {
|
||||||
|
// Arrange
|
||||||
|
const stringErr: Result<number, string> = err('error') as Result<number, string>;
|
||||||
|
const numberErr: Result<number, number> = err(404) as Result<number, number>;
|
||||||
|
const objectErr: Result<number, { code: string }> = err({ code: 'ERR' }) as Result<number, { code: string }>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(stringErr.isErr()).toBe(true);
|
||||||
|
expect(numberErr.isErr()).toBe(true);
|
||||||
|
expect(objectErr.isErr()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Mutual exclusivity', () => {
|
||||||
|
it('should be mutually exclusive for Ok instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
ok(42),
|
||||||
|
ok('hello'),
|
||||||
|
ok(null),
|
||||||
|
ok(undefined),
|
||||||
|
ok(false),
|
||||||
|
ok(0)
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(result => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
expect(result.isErr()).toBe(false);
|
||||||
|
expect(result.isOk() && result.isErr()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be mutually exclusive for Err instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
err('error'),
|
||||||
|
err(404),
|
||||||
|
err(null),
|
||||||
|
err(undefined),
|
||||||
|
err(new Error('test'))
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(result => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(result.isOk()).toBe(false);
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
expect(result.isOk() || result.isErr()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Type narrowing in control flow', () => {
|
||||||
|
it('should enable type-safe value access in if statements', () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<{ name: string; age: number }, string> = ok({ name: 'John', age: 30 }) as Result<{ name: string; age: number }, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value.name).toBe('John');
|
||||||
|
expect(result.value.age).toBe(30);
|
||||||
|
expect(typeof result.value.name).toBe('string');
|
||||||
|
expect(typeof result.value.age).toBe('number');
|
||||||
|
} else {
|
||||||
|
fail('Expected result to be Ok');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with early returns', () => {
|
||||||
|
// Arrange
|
||||||
|
function processResult(result: Result<number, string>): string {
|
||||||
|
if (result.isErr()) {
|
||||||
|
return `error: ${result.err}`;
|
||||||
|
}
|
||||||
|
return `value: ${result.value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const okResult: Result<number, string> = ok(42) as Result<number, string>;
|
||||||
|
const errResult: Result<number, string> = err('failed') as Result<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const okOutput = processResult(okResult);
|
||||||
|
const errOutput = processResult(errResult);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(okOutput).toBe('value: 42');
|
||||||
|
expect(errOutput).toBe('error: failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work in complex conditional logic', () => {
|
||||||
|
// Arrange
|
||||||
|
const result1: Result<number, string> = ok(10) as Result<number, string>;
|
||||||
|
const result2: Result<string, string> = ok('test') as Result<string, string>;
|
||||||
|
const result3: Result<boolean, string> = err('no value') as Result<boolean, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result1.isOk() && result2.isOk()) {
|
||||||
|
expect(result1.value + result2.value.length).toBe(14);
|
||||||
|
} else {
|
||||||
|
fail('Expected both results to be Ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result1.isOk() && result3.isErr()) {
|
||||||
|
expect(result1.value).toBe(10);
|
||||||
|
expect(result3.err).toBe('no value');
|
||||||
|
} else {
|
||||||
|
fail('Expected result1 to be Ok and result3 to be Err');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with negated conditions', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult: Result<string, number> = ok('hello') as Result<string, number>;
|
||||||
|
const errResult: Result<string, number> = err(404) as Result<string, number>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (!okResult.isErr()) {
|
||||||
|
expect(okResult.value).toBe('hello');
|
||||||
|
} else {
|
||||||
|
fail('Expected result to be Ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!errResult.isOk()) {
|
||||||
|
expect(errResult.isErr()).toBe(true);
|
||||||
|
} else {
|
||||||
|
fail('Expected result to be Err');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Performance and consistency', () => {
|
||||||
|
it('should return consistent results across multiple calls', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult = ok(42);
|
||||||
|
const errResult = err('error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(okResult.isOk()).toBe(okResult.isOk());
|
||||||
|
expect(okResult.isErr()).toBe(okResult.isErr());
|
||||||
|
expect(errResult.isOk()).toBe(errResult.isOk());
|
||||||
|
expect(errResult.isErr()).toBe(errResult.isErr());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be callable without side effects', () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ok({ counter: 0 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const before = result.value.counter;
|
||||||
|
result.isOk();
|
||||||
|
result.isErr();
|
||||||
|
result.isOk();
|
||||||
|
const after = result.value.counter;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(before).toBe(after);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge cases', () => {
|
||||||
|
it('should handle deeply nested objects', () => {
|
||||||
|
// Arrange
|
||||||
|
const deepObject = {
|
||||||
|
level1: {
|
||||||
|
level2: {
|
||||||
|
level3: {
|
||||||
|
value: 'deep'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result: Result<typeof deepObject, string> = ok(deepObject) as Result<typeof deepObject, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value.level1.level2.level3.value).toBe('deep');
|
||||||
|
} else {
|
||||||
|
fail('Expected result to be Ok');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Error objects as error values', () => {
|
||||||
|
// Arrange
|
||||||
|
const error = new Error('Test error');
|
||||||
|
const result: Result<number, Error> = err(error) as Result<number, Error>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBeInstanceOf(Error);
|
||||||
|
expect(result.err.message).toBe('Test error');
|
||||||
|
} else {
|
||||||
|
fail('Expected result to be Err');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
188
src/result/spec/is.type-spec.ts
Normal file
188
src/result/spec/is.type-spec.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Result, Ok, Err, ok, err } from "../";
|
||||||
|
|
||||||
|
describe("Result isOk/isErr - Type Tests", () => {
|
||||||
|
describe("isOk() type guard", () => {
|
||||||
|
it("should act as type guard narrowing Result<T, E> to Ok<T>", () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<number, string> = ok(42) as Result<number, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result.isOk()) {
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Ok<number>>();
|
||||||
|
expectTypeOf(result.value).toEqualTypeOf<number>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return type predicate 'this is Ok<T>'", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberResult: Result<number, string> = ok(42) as Result<number, string>;
|
||||||
|
const stringResult: Result<string, number> = ok("hello") as Result<string, number>;
|
||||||
|
const objectResult: Result<{ id: number }, string> = ok({ id: 1 }) as Result<{ id: number }, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberCheck = numberResult.isOk();
|
||||||
|
const stringCheck = stringResult.isOk();
|
||||||
|
const objectCheck = objectResult.isOk();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberCheck).toEqualTypeOf<boolean>();
|
||||||
|
expectTypeOf(stringCheck).toEqualTypeOf<boolean>();
|
||||||
|
expectTypeOf(objectCheck).toEqualTypeOf<boolean>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should enable access to value property after type narrowing", () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<{ name: string; age: number }, string> = ok({ name: "John", age: 30 }) as Result<{ name: string; age: number }, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result.isOk()) {
|
||||||
|
expectTypeOf(result.value).toEqualTypeOf<{ name: string; age: number }>();
|
||||||
|
expectTypeOf(result.value.name).toEqualTypeOf<string>();
|
||||||
|
expectTypeOf(result.value.age).toEqualTypeOf<number>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with complex generic types", () => {
|
||||||
|
// Arrange
|
||||||
|
type ComplexType = Array<{ id: number; tags: string[] }>;
|
||||||
|
const result: Result<ComplexType, Error> = ok([{ id: 1, tags: ["a", "b"] }]) as Result<ComplexType, Error>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result.isOk()) {
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Ok<ComplexType>>();
|
||||||
|
expectTypeOf(result.value).toEqualTypeOf<ComplexType>();
|
||||||
|
expectTypeOf(result.value[0]).toEqualTypeOf<{ id: number; tags: string[] }>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with union types", () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<string | number, boolean> = ok("hello") as Result<string | number, boolean>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result.isOk()) {
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Ok<string | number>>();
|
||||||
|
expectTypeOf(result.value).toEqualTypeOf<string | number>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isErr() type guard", () => {
|
||||||
|
it("should act as type guard narrowing Result<T, E> to Err<E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<number, string> = err("error") as Result<number, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result.isErr()) {
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Err<string>>();
|
||||||
|
expectTypeOf(result.err).toEqualTypeOf<string>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return type predicate 'this is Err<E>'", () => {
|
||||||
|
// Arrange
|
||||||
|
const stringErrResult: Result<number, string> = err("error") as Result<number, string>;
|
||||||
|
const numberErrResult: Result<string, number> = err(404) as Result<string, number>;
|
||||||
|
const objectErrResult: Result<number, { code: string }> = err({ code: "ERR" }) as Result<number, { code: string }>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringCheck = stringErrResult.isErr();
|
||||||
|
const numberCheck = numberErrResult.isErr();
|
||||||
|
const objectCheck = objectErrResult.isErr();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringCheck).toEqualTypeOf<boolean>();
|
||||||
|
expectTypeOf(numberCheck).toEqualTypeOf<boolean>();
|
||||||
|
expectTypeOf(objectCheck).toEqualTypeOf<boolean>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should narrow to Err with correct error type", () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<number, { code: number; message: string }> = err({ code: 500, message: "Internal" }) as Result<number, { code: number; message: string }>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result.isErr()) {
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Err<{ code: number; message: string }>>();
|
||||||
|
expectTypeOf(result.err).toEqualTypeOf<{ code: number; message: string }>();
|
||||||
|
expectTypeOf(result.err.code).toEqualTypeOf<number>();
|
||||||
|
expectTypeOf(result.err.message).toEqualTypeOf<string>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Type narrowing patterns", () => {
|
||||||
|
it("should enable exhaustive type checking with if-else", () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<string, number> = ok("test") as Result<string, number>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result.isOk()) {
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Ok<string>>();
|
||||||
|
expectTypeOf(result.value).toEqualTypeOf<string>();
|
||||||
|
} else {
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Err<number>>();
|
||||||
|
expectTypeOf(result.err).toEqualTypeOf<number>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with early returns", () => {
|
||||||
|
// Arrange
|
||||||
|
function processResult(result: Result<number, string>): number {
|
||||||
|
if (result.isErr()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return result.value * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testResult: Result<number, string> = ok(5) as Result<number, string>;
|
||||||
|
const output = processResult(testResult);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(output).toEqualTypeOf<number>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work in complex conditional logic", () => {
|
||||||
|
// Arrange
|
||||||
|
const result1: Result<number, string> = ok(1) as Result<number, string>;
|
||||||
|
const result2: Result<string, string> = ok("test") as Result<string, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result1.isOk() && result2.isOk()) {
|
||||||
|
expectTypeOf(result1).toEqualTypeOf<Ok<number>>();
|
||||||
|
expectTypeOf(result2).toEqualTypeOf<Ok<string>>();
|
||||||
|
expectTypeOf(result1.value).toEqualTypeOf<number>();
|
||||||
|
expectTypeOf(result2.value).toEqualTypeOf<string>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with negated conditions", () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<boolean, string> = ok(true) as Result<boolean, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (!result.isErr()) {
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Ok<boolean>>();
|
||||||
|
expectTypeOf(result.value).toEqualTypeOf<boolean>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.isOk()) {
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Err<string>>();
|
||||||
|
expectTypeOf(result.err).toEqualTypeOf<string>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Method chaining with type narrowing", () => {
|
||||||
|
it("should preserve type information in method chains", () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<number, string> = ok(42) as Result<number, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result.isOk()) {
|
||||||
|
const mapped = result.map(x => x.toString());
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<Ok<string>>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
244
src/result/spec/map.spec.ts
Normal file
244
src/result/spec/map.spec.ts
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Result, Ok, Err, ok, err } from '../';
|
||||||
|
|
||||||
|
describe('Result map and mapErr - Runtime Tests', () => {
|
||||||
|
describe('map method', () => {
|
||||||
|
describe('when Result is Ok', () => {
|
||||||
|
it('should transform the value using the provided function', () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ok(5);
|
||||||
|
const mapper = (x: number) => x * 2;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mapped = result.map(mapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mapped.isOk()).toBe(true);
|
||||||
|
if (mapped.isOk()) {
|
||||||
|
expect(mapped.value).toBe(10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle different type transformations', () => {
|
||||||
|
// Arrange
|
||||||
|
const numberResult = ok(42);
|
||||||
|
const stringResult = ok('hello');
|
||||||
|
const booleanResult = ok(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberToString = numberResult.map(x => x.toString());
|
||||||
|
const stringToNumber = stringResult.map(s => s.length);
|
||||||
|
const booleanToString = booleanResult.map(b => b ? 'yes' : 'no');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(numberToString.isOk()).toBe(true);
|
||||||
|
expect(stringToNumber.isOk()).toBe(true);
|
||||||
|
expect(booleanToString.isOk()).toBe(true);
|
||||||
|
|
||||||
|
if (numberToString.isOk()) expect(numberToString.value).toBe('42');
|
||||||
|
if (stringToNumber.isOk()) expect(stringToNumber.value).toBe(5);
|
||||||
|
if (booleanToString.isOk()) expect(booleanToString.value).toBe('yes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex object transformations', () => {
|
||||||
|
// Arrange
|
||||||
|
const user = { id: 1, name: 'John', age: 30 };
|
||||||
|
const userResult = ok(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const userSummary = userResult.map(u => ({
|
||||||
|
displayName: `${u.name} (${u.age})`,
|
||||||
|
isAdult: u.age >= 18
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(userSummary.isOk()).toBe(true);
|
||||||
|
if (userSummary.isOk()) {
|
||||||
|
expect(userSummary.value).toEqual({
|
||||||
|
displayName: 'John (30)',
|
||||||
|
isAdult: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not mutate the original result', () => {
|
||||||
|
// Arrange
|
||||||
|
const original = ok({ count: 5 });
|
||||||
|
const originalValue = original.value;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mapped = original.map(obj => ({ ...obj, count: obj.count * 2 }));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(original.value).toBe(originalValue);
|
||||||
|
expect(original.value.count).toBe(5);
|
||||||
|
if (mapped.isOk()) {
|
||||||
|
expect(mapped.value.count).toBe(10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Result is Err', () => {
|
||||||
|
it('should return Err without calling the mapper function', () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult: Result<number, string> = err('error') as Result<number, string>;
|
||||||
|
const mapperSpy = vi.fn((x: number) => x * 2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = errResult.map(mapperSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
expect(mapperSpy).not.toHaveBeenCalled();
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve Err through multiple map operations', () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult: Result<number, string> = err('error') as Result<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = errResult
|
||||||
|
.map(x => x * 2)
|
||||||
|
.map(x => x.toString())
|
||||||
|
.map(s => s.length);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mapErr method', () => {
|
||||||
|
describe('when Result is Err', () => {
|
||||||
|
it('should transform the error using the provided function', () => {
|
||||||
|
// Arrange
|
||||||
|
const result = err('error');
|
||||||
|
const mapper = (e: string) => e.toUpperCase();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mapped = result.mapErr(mapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mapped.isErr()).toBe(true);
|
||||||
|
if (mapped.isErr()) {
|
||||||
|
expect(mapped.err).toBe('ERROR');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle different error type transformations', () => {
|
||||||
|
// Arrange
|
||||||
|
const stringErr = err('not found');
|
||||||
|
const numberErr = err(404);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const toObject = stringErr.mapErr(e => ({ message: e }));
|
||||||
|
const toMessage = numberErr.mapErr(code => `Error code: ${code}`);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(toObject.isErr()).toBe(true);
|
||||||
|
expect(toMessage.isErr()).toBe(true);
|
||||||
|
|
||||||
|
if (toObject.isErr()) expect(toObject.err).toEqual({ message: 'not found' });
|
||||||
|
if (toMessage.isErr()) expect(toMessage.err).toBe('Error code: 404');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not mutate the original error', () => {
|
||||||
|
// Arrange
|
||||||
|
const original = err({ code: 500, message: 'Internal' });
|
||||||
|
const originalErr = original.err;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mapped = original.mapErr(e => ({ ...e, code: 503 }));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(original.err).toBe(originalErr);
|
||||||
|
expect(original.err.code).toBe(500);
|
||||||
|
if (mapped.isErr()) {
|
||||||
|
expect(mapped.err.code).toBe(503);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Result is Ok', () => {
|
||||||
|
it('should return Ok without calling the mapper function', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult: Result<number, string> = ok(42) as Result<number, string>;
|
||||||
|
const mapperSpy = vi.fn((e: string) => e.toUpperCase());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = okResult.mapErr(mapperSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
expect(mapperSpy).not.toHaveBeenCalled();
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve Ok through multiple mapErr operations', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult: Result<number, string> = ok(42) as Result<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = okResult
|
||||||
|
.mapErr(e => e.toUpperCase())
|
||||||
|
.mapErr(e => e.length)
|
||||||
|
.mapErr(n => `Error length: ${n}`);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('map and mapErr integration', () => {
|
||||||
|
it('should allow chaining map and mapErr together', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult: Result<number, string> = ok(5) as Result<number, string>;
|
||||||
|
const errResult: Result<number, string> = err('error') as Result<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mappedOk = okResult
|
||||||
|
.map(x => x * 2)
|
||||||
|
.mapErr(e => e.toUpperCase());
|
||||||
|
|
||||||
|
const mappedErr = errResult
|
||||||
|
.map(x => x * 2)
|
||||||
|
.mapErr(e => e.toUpperCase());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mappedOk.isOk()).toBe(true);
|
||||||
|
if (mappedOk.isOk()) expect(mappedOk.value).toBe(10);
|
||||||
|
|
||||||
|
expect(mappedErr.isErr()).toBe(true);
|
||||||
|
if (mappedErr.isErr()) expect(mappedErr.err).toBe('ERROR');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle edge cases with falsy values', () => {
|
||||||
|
// Arrange
|
||||||
|
const emptyStringResult = ok('');
|
||||||
|
const zeroResult = ok(0);
|
||||||
|
const falseResult = ok(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringMapped = emptyStringResult.map(s => s.length);
|
||||||
|
const numberMapped = zeroResult.map(n => n.toString());
|
||||||
|
const booleanMapped = falseResult.map(b => !b);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if (stringMapped.isOk()) expect(stringMapped.value).toBe(0);
|
||||||
|
if (numberMapped.isOk()) expect(numberMapped.value).toBe('0');
|
||||||
|
if (booleanMapped.isOk()) expect(booleanMapped.value).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
231
src/result/spec/map.type-spec.ts
Normal file
231
src/result/spec/map.type-spec.ts
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Result, Ok, Err, ok, err } from "../";
|
||||||
|
|
||||||
|
describe("Result map and mapErr - Type Tests", () => {
|
||||||
|
describe("map method", () => {
|
||||||
|
it("should transform Ok<T> to Result<U, E> with correct type inference", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberResult = ok(42);
|
||||||
|
const stringResult = ok("hello");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const doubledNumber = numberResult.map(x => x * 2);
|
||||||
|
const uppercaseString = stringResult.map(s => s.toUpperCase());
|
||||||
|
const numberToString = numberResult.map(x => x.toString());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(doubledNumber).toEqualTypeOf<Ok<number>>();
|
||||||
|
expectTypeOf(uppercaseString).toEqualTypeOf<Ok<string>>();
|
||||||
|
expectTypeOf(numberToString).toEqualTypeOf<Ok<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve error type through map on Result<T, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<number, string> = ok(42) as Result<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mapped = result.map(x => x * 2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<Result<number, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex type transformations", () => {
|
||||||
|
// Arrange
|
||||||
|
type User = { id: number; name: string };
|
||||||
|
type UserDto = { userId: number; displayName: string };
|
||||||
|
const userResult = ok({ id: 1, name: "John" } as User);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const userDtoResult = userResult.map((user): UserDto => ({
|
||||||
|
userId: user.id,
|
||||||
|
displayName: user.name.toUpperCase()
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(userDtoResult).toEqualTypeOf<Ok<UserDto>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve literal types when appropriate", () => {
|
||||||
|
// Arrange
|
||||||
|
const literalResult = ok("success" as const);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mappedLiteral = literalResult.map(x => x);
|
||||||
|
const transformedLiteral = literalResult.map(x => `status: ${x}` as const);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(mappedLiteral).toEqualTypeOf<Ok<"success">>();
|
||||||
|
expectTypeOf(transformedLiteral).toEqualTypeOf<Ok<"status: success">>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with union types", () => {
|
||||||
|
// Arrange
|
||||||
|
const unionResult: Result<string | number, boolean> = ok("hello" as string | number) as Result<string | number, boolean>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringified = unionResult.map(x => String(x));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringified).toEqualTypeOf<Result<string, boolean>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with correct mapper function signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberResult = ok(42);
|
||||||
|
const stringResult = ok("hello");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberResult.map).toBeCallableWith((x: number) => x * 2);
|
||||||
|
expectTypeOf(numberResult.map).toBeCallableWith((x: number) => x.toString());
|
||||||
|
expectTypeOf(stringResult.map).toBeCallableWith((s: string) => s.length);
|
||||||
|
|
||||||
|
// @ts-expect-error - wrong parameter type for map
|
||||||
|
numberResult.map((s: string) => s.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("mapErr method", () => {
|
||||||
|
it("should transform Err<E> to Result<T, F> with correct type inference", () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult = err("error");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mappedErr = errResult.mapErr(e => e.toUpperCase());
|
||||||
|
const errToNumber = errResult.mapErr(e => e.length);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(mappedErr).toEqualTypeOf<Err<string>>();
|
||||||
|
expectTypeOf(errToNumber).toEqualTypeOf<Err<number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve value type through mapErr on Result<T, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<number, string> = err("error") as Result<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mapped = result.mapErr(e => e.length);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<Result<number, number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex error transformations", () => {
|
||||||
|
// Arrange
|
||||||
|
type ApiError = { code: number; message: string };
|
||||||
|
type DisplayError = { title: string; description: string };
|
||||||
|
const errResult = err({ code: 404, message: "Not found" } as ApiError);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const displayErr = errResult.mapErr((e): DisplayError => ({
|
||||||
|
title: `Error ${e.code}`,
|
||||||
|
description: e.message
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(displayErr).toEqualTypeOf<Err<DisplayError>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with correct mapper function signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult: Result<number, string> = err("error") as Result<number, string>;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(errResult.mapErr).toBeCallableWith((e: string) => e.toUpperCase());
|
||||||
|
expectTypeOf(errResult.mapErr).toBeCallableWith((e: string) => e.length);
|
||||||
|
|
||||||
|
// @ts-expect-error - wrong parameter type for mapErr
|
||||||
|
errResult.mapErr((n: number) => n * 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("narrow return types on concrete variants", () => {
|
||||||
|
it("should return Ok<U> when map is called on a known Ok", () => {
|
||||||
|
const mapped = ok(42).map(x => x.toString());
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<Ok<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Ok<T> when mapErr is called on a known Ok", () => {
|
||||||
|
const mapped = ok(42).mapErr(e => String(e));
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<Ok<number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Ok<T> when flatMapErr is called on a known Ok", () => {
|
||||||
|
const mapped = ok(42).flatMapErr(e => err(String(e)));
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<Ok<number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Err<E> when map is called on a known Err", () => {
|
||||||
|
const mapped = err("fail").map(x => String(x));
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<Err<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Err<F> when mapErr is called on a known Err", () => {
|
||||||
|
const mapped = err("fail").mapErr(e => e.length);
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<Err<number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Err<E> when flatMap is called on a known Err", () => {
|
||||||
|
const mapped = err("fail").flatMap(x => ok(String(x)));
|
||||||
|
expectTypeOf(mapped).toEqualTypeOf<Err<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Err<E> when filter is called on a known Err", () => {
|
||||||
|
const filtered = err("fail").filter();
|
||||||
|
expectTypeOf(filtered).toEqualTypeOf<Err<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Ok<U> when flatMap callback returns Ok", () => {
|
||||||
|
const result = ok(5).flatMap(x => ok(x.toString()));
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Ok<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Result<U, never> when flatMap callback returns Result", () => {
|
||||||
|
const result = ok(5).flatMap(x => (x > 0 ? ok(x) : err("neg")) as Result<number, never>);
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Result<number, never>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Err<F> when flatMapErr callback returns Err", () => {
|
||||||
|
const result = err("fail").flatMapErr(e => err(e.length));
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Err<number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Result<never, F> when flatMapErr callback returns Result", () => {
|
||||||
|
const result = err("fail").flatMapErr(e => (e === "retry" ? ok(0) : err(e.length)) as Result<never, number>);
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Result<never, number>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("map and mapErr interaction", () => {
|
||||||
|
it("should work together in chains with correct type inference", () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<number, string> = ok(5) as Result<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const chained = result
|
||||||
|
.map(x => x * 2)
|
||||||
|
.mapErr(e => e.toUpperCase());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(chained).toEqualTypeOf<Result<number, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle mixed transformations correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
type Input = { value: string };
|
||||||
|
type Output = { parsed: number };
|
||||||
|
type InputError = string;
|
||||||
|
type OutputError = { code: number; message: string };
|
||||||
|
|
||||||
|
const result: Result<Input, InputError> = ok({ value: "42" }) as Result<Input, InputError>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const transformed = result
|
||||||
|
.map((input): Output => ({ parsed: parseInt(input.value) }))
|
||||||
|
.mapErr((e): OutputError => ({ code: 400, message: e }));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(transformed).toEqualTypeOf<Result<Output, OutputError>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
270
src/result/spec/match.spec.ts
Normal file
270
src/result/spec/match.spec.ts
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Result, Ok, Err, ok, err } from '../';
|
||||||
|
|
||||||
|
describe('Result.match - Runtime Tests', () => {
|
||||||
|
describe('match with Ok', () => {
|
||||||
|
it('should execute onOk callback with the contained value', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult = ok(42);
|
||||||
|
const onOk = vi.fn((value: number) => `Value: ${value}`);
|
||||||
|
const onErr = vi.fn(() => 'Error occurred');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = okResult.match(onOk, onErr);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(onOk).toHaveBeenCalledWith(42);
|
||||||
|
expect(onOk).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onErr).not.toHaveBeenCalled();
|
||||||
|
expect(result).toBe('Value: 42');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the result of onOk callback', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult = ok(10);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = okResult.match(
|
||||||
|
(value) => value * 2,
|
||||||
|
() => 0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with various data types', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
{ result: ok(42), expected: 'number: 42' },
|
||||||
|
{ result: ok('hello'), expected: 'string: "hello"' },
|
||||||
|
{ result: ok(true), expected: 'boolean: true' },
|
||||||
|
{ result: ok({ id: 1 }), expected: 'object: {"id":1}' },
|
||||||
|
{ result: ok([1, 2, 3]), expected: 'object: [1,2,3]' },
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ result: r, expected }) => {
|
||||||
|
// Act
|
||||||
|
const output = r.match(
|
||||||
|
(value) => `${typeof value}: ${JSON.stringify(value)}`,
|
||||||
|
() => 'error'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(output).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null and undefined values correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const nullResult = ok(null);
|
||||||
|
const undefinedResult = ok(undefined);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const nullOutput = nullResult.match(
|
||||||
|
(value) => `Got null: ${value}`,
|
||||||
|
() => 'Error'
|
||||||
|
);
|
||||||
|
|
||||||
|
const undefinedOutput = undefinedResult.match(
|
||||||
|
(value) => `Got undefined: ${value}`,
|
||||||
|
() => 'Error'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(nullOutput).toBe('Got null: null');
|
||||||
|
expect(undefinedOutput).toBe('Got undefined: undefined');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex transformations', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult = ok({ name: 'John', age: 30 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = okResult.match(
|
||||||
|
(person) => ({
|
||||||
|
greeting: `Hello, ${person.name}!`,
|
||||||
|
isAdult: person.age >= 18,
|
||||||
|
category: person.age < 18 ? 'minor' : person.age < 65 ? 'adult' : 'senior'
|
||||||
|
}),
|
||||||
|
() => ({ greeting: 'Hello, stranger!', isAdult: false, category: 'unknown' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual({
|
||||||
|
greeting: 'Hello, John!',
|
||||||
|
isAdult: true,
|
||||||
|
category: 'adult'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('match with Err', () => {
|
||||||
|
it('should execute onErr callback with the contained error', () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult: Result<number, string> = err('something failed');
|
||||||
|
const onOk = vi.fn((value: number) => `Value: ${value}`);
|
||||||
|
const onErr = vi.fn((error: string) => `Error: ${error}`);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = errResult.match(onOk, onErr);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(onErr).toHaveBeenCalledWith('something failed');
|
||||||
|
expect(onErr).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onOk).not.toHaveBeenCalled();
|
||||||
|
expect(result).toBe('Error: something failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the result of onErr callback', () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult: Result<string, string> = err('failed') as Result<string, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = errResult.match(
|
||||||
|
(value) => value.toUpperCase(),
|
||||||
|
() => 'DEFAULT'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('DEFAULT');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle different error types in onErr', () => {
|
||||||
|
// Arrange
|
||||||
|
const stringErr: Result<number, string> = err('error') as Result<number, string>;
|
||||||
|
const numberErr: Result<number, number> = err(404) as Result<number, number>;
|
||||||
|
const objectErr: Result<number, { code: number }> = err({ code: 500 }) as Result<number, { code: number }>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringOutput = stringErr.match(
|
||||||
|
(value) => value.toString(),
|
||||||
|
(error) => `String error: ${error}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const numberOutput = numberErr.match(
|
||||||
|
(value) => value,
|
||||||
|
(error) => error * -1
|
||||||
|
);
|
||||||
|
|
||||||
|
const objectOutput = objectErr.match(
|
||||||
|
(value): { value: number | null; code?: number } => ({ value }),
|
||||||
|
(error): { value: number | null; code?: number } => ({ value: null, code: error.code })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(stringOutput).toBe('String error: error');
|
||||||
|
expect(numberOutput).toBe(-404);
|
||||||
|
expect(objectOutput).toEqual({ value: null, code: 500 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('match with mixed Result types', () => {
|
||||||
|
it('should handle dynamic result types correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<string, string>[] = [
|
||||||
|
ok('hello') as Result<string, string>,
|
||||||
|
err('error') as Result<string, string>,
|
||||||
|
ok('world') as Result<string, string>
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const outputs = results.map(r =>
|
||||||
|
r.match(
|
||||||
|
(value) => value.toUpperCase(),
|
||||||
|
(error) => `ERR: ${error}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(outputs).toEqual(['HELLO', 'ERR: error', 'WORLD']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work in conditional contexts', () => {
|
||||||
|
// Arrange
|
||||||
|
const getValue = (hasValue: boolean): Result<number, string> =>
|
||||||
|
hasValue ? ok(42) as Result<number, string> : err('not found') as Result<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const withValue = getValue(true).match(
|
||||||
|
(value) => `Found: ${value}`,
|
||||||
|
(error) => `Error: ${error}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const withoutValue = getValue(false).match(
|
||||||
|
(value) => `Found: ${value}`,
|
||||||
|
(error) => `Error: ${error}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(withValue).toBe('Found: 42');
|
||||||
|
expect(withoutValue).toBe('Error: not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('match error handling', () => {
|
||||||
|
it('should propagate errors from onOk callback', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult = ok(42);
|
||||||
|
const error = new Error('onOk error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(() => {
|
||||||
|
okResult.match(
|
||||||
|
() => { throw error; },
|
||||||
|
() => 'fallback'
|
||||||
|
);
|
||||||
|
}).toThrow('onOk error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should propagate errors from onErr callback', () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult: Result<number, string> = err('error') as Result<number, string>;
|
||||||
|
const error = new Error('onErr error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(() => {
|
||||||
|
errResult.match(
|
||||||
|
(value) => value.toString(),
|
||||||
|
() => { throw error; }
|
||||||
|
);
|
||||||
|
}).toThrow('onErr error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('match with async callbacks', () => {
|
||||||
|
it('should handle async onOk callback', async () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult = ok(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await okResult.match(
|
||||||
|
async (value) => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
|
return `Async: ${value}`;
|
||||||
|
},
|
||||||
|
async () => 'Async: Error'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('Async: 42');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle async onErr callback', async () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult: Result<number, string> = err('error');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await errResult.match(
|
||||||
|
async (value) => `Async: ${value}`,
|
||||||
|
async (error) => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
|
return `Async error: ${error}`;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('Async error: error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
230
src/result/spec/match.type-spec.ts
Normal file
230
src/result/spec/match.type-spec.ts
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Result, ResultInterface, Ok, Err, ok, err } from "../";
|
||||||
|
|
||||||
|
// Helper to use the interface signatures which correctly unify T and E
|
||||||
|
const ri = <T, E>(r: Result<T, E>) => r as unknown as ResultInterface<T, E>;
|
||||||
|
|
||||||
|
describe("Result.match - Type Tests", () => {
|
||||||
|
describe("match with Ok", () => {
|
||||||
|
it("should infer return type from both callback return types", () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult = ok(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringResult = okResult.match(
|
||||||
|
(value) => `Value: ${value}`,
|
||||||
|
() => "Error"
|
||||||
|
);
|
||||||
|
|
||||||
|
const numberResult = okResult.match(
|
||||||
|
(value) => value * 2,
|
||||||
|
() => 0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<string>();
|
||||||
|
expectTypeOf(numberResult).toEqualTypeOf<number>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should infer union type when callbacks return different types", () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const unionResult = result.match<string | number>(
|
||||||
|
(value) => value.toString(),
|
||||||
|
() => 0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(unionResult).toEqualTypeOf<string | number>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve complex return types", () => {
|
||||||
|
// Arrange
|
||||||
|
type ComplexType = { data: string[]; count: number };
|
||||||
|
const okResult = ok({ id: 1, name: "test" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const complexResult = okResult.match(
|
||||||
|
(value): ComplexType => ({ data: [value.name], count: 1 }),
|
||||||
|
(): ComplexType => ({ data: [], count: 0 })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(complexResult).toEqualTypeOf<ComplexType>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle generic types correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult = ok([1, 2, 3]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mappedResult = okResult.match(
|
||||||
|
(arr) => arr.map(x => x.toString()),
|
||||||
|
() => []
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(mappedResult).toEqualTypeOf<string[]>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("match with Err", () => {
|
||||||
|
it("should infer return type from callback return types", () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult = ri<number, string>(err("error") as Result<number, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringResult = errResult.match(
|
||||||
|
(value) => `Value: ${value}`,
|
||||||
|
(error) => `Error: ${error}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<string>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle union return types with Err", () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult = ri<string, string>(err("error") as Result<string, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const unionResult = errResult.match<number | string>(
|
||||||
|
(value) => value.length,
|
||||||
|
() => "empty"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(unionResult).toEqualTypeOf<number | string>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("match callback parameter types", () => {
|
||||||
|
it("should provide correct parameter type to onOk callback", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberResult = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
const stringResult = ri<string, number>(ok("hello") as Result<string, number>);
|
||||||
|
const objectResult = ri<{ id: number; name: string }, string>(ok({ id: 1, name: "test" }) as Result<{ id: number; name: string }, string>);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
numberResult.match(
|
||||||
|
(value) => {
|
||||||
|
expectTypeOf(value).toEqualTypeOf<number>();
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
() => 0
|
||||||
|
);
|
||||||
|
|
||||||
|
stringResult.match(
|
||||||
|
(value) => {
|
||||||
|
expectTypeOf(value).toEqualTypeOf<string>();
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
() => ""
|
||||||
|
);
|
||||||
|
|
||||||
|
objectResult.match(
|
||||||
|
(value) => {
|
||||||
|
expectTypeOf(value).toEqualTypeOf<{ id: number; name: string }>();
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
() => ({ id: 0, name: "" })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should provide correct parameter type to onErr callback", () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ri<number, { code: number; message: string }>(err({ code: 404, message: "Not found" }) as Result<number, { code: number; message: string }>);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
result.match(
|
||||||
|
(value) => value,
|
||||||
|
(error) => {
|
||||||
|
expectTypeOf(error).toEqualTypeOf<{ code: number; message: string }>();
|
||||||
|
return error.code;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should require compatible return types from both callbacks", () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
|
||||||
|
// Act & Assert - these should not raise errors (valid calls)
|
||||||
|
result.match(
|
||||||
|
(value) => value.toString(),
|
||||||
|
() => "default"
|
||||||
|
);
|
||||||
|
|
||||||
|
result.match(
|
||||||
|
(value) => value,
|
||||||
|
() => 0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("match function signature validation", () => {
|
||||||
|
it("should be callable with correct callback signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
const onOk = (value: number) => value * 2;
|
||||||
|
const onErr = (error: string) => error.length;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result.match).toBeCallableWith(onOk, onErr);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be callable with incorrect callback signatures", () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// @ts-expect-error - wrong parameter type for onOk
|
||||||
|
result.match((value: string) => value, () => "");
|
||||||
|
// @ts-expect-error - wrong parameter type for onErr
|
||||||
|
result.match((x: number) => x, (value: number) => value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be callable with missing callbacks", () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// @ts-expect-error - missing onErr callback
|
||||||
|
result.match((x: number) => x);
|
||||||
|
// @ts-expect-error - missing all callbacks
|
||||||
|
result.match();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("match with complex types", () => {
|
||||||
|
it("should handle async callbacks correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult = ok(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const asyncResult = okResult.match(
|
||||||
|
async (value) => `Async: ${value}`,
|
||||||
|
async () => "Async: Error"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(asyncResult).toEqualTypeOf<Promise<string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle callback functions as return values", () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const functionResult = result.match(
|
||||||
|
(value) => (x: number) => x + value,
|
||||||
|
() => (x: number) => x
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(functionResult).toEqualTypeOf<(x: number) => number>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
510
src/result/spec/namespace.spec.ts
Normal file
510
src/result/spec/namespace.spec.ts
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Result, ResultInterface, Ok, Err, ok, err } from '../';
|
||||||
|
|
||||||
|
describe('Result namespace - Runtime Tests', () => {
|
||||||
|
describe('Result.from', () => {
|
||||||
|
it('should return Ok when function succeeds', () => {
|
||||||
|
// Arrange
|
||||||
|
const fn = () => 42;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.from(fn);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Err when function throws', () => {
|
||||||
|
// Arrange
|
||||||
|
const error = new Error('test error');
|
||||||
|
const fn = () => { throw error; };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.from(fn);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply errorMap when function throws and errorMap is provided', () => {
|
||||||
|
// Arrange
|
||||||
|
const fn = () => { throw new Error('original'); };
|
||||||
|
const errorMap = (e: unknown) => `Mapped: ${(e as Error).message}`;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.from(fn, errorMap);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('Mapped: original');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call errorMap when function succeeds', () => {
|
||||||
|
// Arrange
|
||||||
|
const fn = () => 42;
|
||||||
|
const errorMapSpy = vi.fn((e: unknown) => `Mapped: ${e}`);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.from(fn, errorMapSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
expect(errorMapSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle JSON.parse correctly', () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const validResult = Result.from(() => JSON.parse('{"valid": true}'));
|
||||||
|
const invalidResult = Result.from(() => JSON.parse('invalid'));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(validResult.isOk()).toBe(true);
|
||||||
|
if (validResult.isOk()) {
|
||||||
|
expect(validResult.value).toEqual({ valid: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(invalidResult.isErr()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle functions returning falsy values', () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const zeroResult = Result.from(() => 0);
|
||||||
|
const falseResult = Result.from(() => false);
|
||||||
|
const emptyResult = Result.from(() => '');
|
||||||
|
const nullResult = Result.from(() => null);
|
||||||
|
const undefinedResult = Result.from(() => undefined);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(zeroResult.isOk()).toBe(true);
|
||||||
|
expect(falseResult.isOk()).toBe(true);
|
||||||
|
expect(emptyResult.isOk()).toBe(true);
|
||||||
|
expect(nullResult.isOk()).toBe(true);
|
||||||
|
expect(undefinedResult.isOk()).toBe(true);
|
||||||
|
|
||||||
|
if (zeroResult.isOk()) expect(zeroResult.value).toBe(0);
|
||||||
|
if (falseResult.isOk()) expect(falseResult.value).toBe(false);
|
||||||
|
if (emptyResult.isOk()) expect(emptyResult.value).toBe('');
|
||||||
|
if (nullResult.isOk()) expect(nullResult.value).toBeNull();
|
||||||
|
if (undefinedResult.isOk()) expect(undefinedResult.value).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Result.isResult', () => {
|
||||||
|
it('should return true for Ok instances', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(Result.isResult(ok(42))).toBe(true);
|
||||||
|
expect(Result.isResult(ok('hello'))).toBe(true);
|
||||||
|
expect(Result.isResult(ok(null))).toBe(true);
|
||||||
|
expect(Result.isResult(new Ok(42))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for Err instances', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(Result.isResult(err('error'))).toBe(true);
|
||||||
|
expect(Result.isResult(err(404))).toBe(true);
|
||||||
|
expect(Result.isResult(err(null))).toBe(true);
|
||||||
|
expect(Result.isResult(new Err('error'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for non-Result values', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(Result.isResult(42)).toBe(false);
|
||||||
|
expect(Result.isResult('hello')).toBe(false);
|
||||||
|
expect(Result.isResult(null)).toBe(false);
|
||||||
|
expect(Result.isResult(undefined)).toBe(false);
|
||||||
|
expect(Result.isResult(true)).toBe(false);
|
||||||
|
expect(Result.isResult({ value: 42 })).toBe(false);
|
||||||
|
expect(Result.isResult([1, 2, 3])).toBe(false);
|
||||||
|
expect(Result.isResult({ isOk: () => true })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Result.all', () => {
|
||||||
|
it('should return Ok with all values when all results are Ok', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [
|
||||||
|
ok(1) as Result<number, string>,
|
||||||
|
ok(2) as Result<number, string>,
|
||||||
|
ok(3) as Result<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.all(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value).toEqual([1, 2, 3]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the first Err when any result is Err', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [
|
||||||
|
ok(1) as Result<number, string>,
|
||||||
|
err('first error') as Result<number, string>,
|
||||||
|
ok(3) as Result<number, string>,
|
||||||
|
err('second error') as Result<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.all(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('first error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Ok with empty array for empty input', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.all(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value).toEqual([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single element arrays', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResults: Result<number, string>[] = [ok(42) as Result<number, string>];
|
||||||
|
const errResults: Result<number, string>[] = [err('error') as Result<number, string>];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const okResult = Result.all(okResults);
|
||||||
|
const errResult = Result.all(errResults);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(okResult.isOk()).toBe(true);
|
||||||
|
if (okResult.isOk()) expect(okResult.value).toEqual([42]);
|
||||||
|
|
||||||
|
expect(errResult.isErr()).toBe(true);
|
||||||
|
if (errResult.isErr()) expect(errResult.err).toBe('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex value types', () => {
|
||||||
|
// Arrange
|
||||||
|
type User = { id: number; name: string };
|
||||||
|
const results: Result<User, string>[] = [
|
||||||
|
ok({ id: 1, name: 'Alice' }) as Result<User, string>,
|
||||||
|
ok({ id: 2, name: 'Bob' }) as Result<User, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.all(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value).toEqual([
|
||||||
|
{ id: 1, name: 'Alice' },
|
||||||
|
{ id: 2, name: 'Bob' },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Result.partition', () => {
|
||||||
|
it('should partition results into values and errors', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [
|
||||||
|
ok(1) as Result<number, string>,
|
||||||
|
err('error1') as Result<number, string>,
|
||||||
|
ok(3) as Result<number, string>,
|
||||||
|
err('error2') as Result<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const partitioned = Result.partition(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(partitioned.values).toEqual([1, 3]);
|
||||||
|
expect(partitioned.errors).toEqual(['error1', 'error2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all Ok results', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [
|
||||||
|
ok(1) as Result<number, string>,
|
||||||
|
ok(2) as Result<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const partitioned = Result.partition(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(partitioned.values).toEqual([1, 2]);
|
||||||
|
expect(partitioned.errors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all Err results', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [
|
||||||
|
err('error1') as Result<number, string>,
|
||||||
|
err('error2') as Result<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const partitioned = Result.partition(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(partitioned.values).toEqual([]);
|
||||||
|
expect(partitioned.errors).toEqual(['error1', 'error2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty array', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const partitioned = Result.partition(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(partitioned.values).toEqual([]);
|
||||||
|
expect(partitioned.errors).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Result.compact', () => {
|
||||||
|
it('should extract values from Ok instances', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [
|
||||||
|
ok(1) as Result<number, string>,
|
||||||
|
err('error') as Result<number, string>,
|
||||||
|
ok(3) as Result<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const values = Result.compact(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(values).toEqual([1, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array when all are Err', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [
|
||||||
|
err('error1') as Result<number, string>,
|
||||||
|
err('error2') as Result<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const values = Result.compact(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(values).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all values when all are Ok', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [
|
||||||
|
ok(1) as Result<number, string>,
|
||||||
|
ok(2) as Result<number, string>,
|
||||||
|
ok(3) as Result<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const values = Result.compact(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(values).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty array', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const values = Result.compact(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(values).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle falsy Ok values', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number | boolean | string, string>[] = [
|
||||||
|
ok(0) as Result<number, string>,
|
||||||
|
ok(false) as Result<boolean, string>,
|
||||||
|
ok('') as Result<string, string>,
|
||||||
|
err('error') as Result<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const values = Result.compact(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(values).toEqual([0, false, '']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Result.some', () => {
|
||||||
|
it('should return true when at least one result is Ok', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [
|
||||||
|
ok(1) as Result<number, string>,
|
||||||
|
err('error') as Result<number, string>,
|
||||||
|
ok(3) as Result<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.some(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when all results are Err', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [
|
||||||
|
err('error1') as Result<number, string>,
|
||||||
|
err('error2') as Result<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.some(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for empty array', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.some(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when all results are Ok', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [
|
||||||
|
ok(1) as Result<number, string>,
|
||||||
|
ok(2) as Result<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.some(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Result.every', () => {
|
||||||
|
it('should return true when all results are Ok', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [
|
||||||
|
ok(1) as Result<number, string>,
|
||||||
|
ok(2) as Result<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.every(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when any result is Err', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [
|
||||||
|
ok(1) as Result<number, string>,
|
||||||
|
err('error') as Result<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.every(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for empty array', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.every(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when all results are Err', () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [
|
||||||
|
err('error1') as Result<number, string>,
|
||||||
|
err('error2') as Result<number, string>,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.every(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Integration tests', () => {
|
||||||
|
it('should work with Result.from and Result.all together', () => {
|
||||||
|
// Arrange
|
||||||
|
const parse = (s: string) => Result.from(() => JSON.parse(s), () => `Invalid JSON: ${s}`);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const allValid = Result.all([
|
||||||
|
parse('{"a": 1}'),
|
||||||
|
parse('{"b": 2}'),
|
||||||
|
]);
|
||||||
|
const someInvalid = Result.all([
|
||||||
|
parse('{"a": 1}'),
|
||||||
|
parse('invalid'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(allValid.isOk()).toBe(true);
|
||||||
|
if (allValid.isOk()) {
|
||||||
|
expect(allValid.value).toEqual([{ a: 1 }, { b: 2 }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(someInvalid.isErr()).toBe(true);
|
||||||
|
if (someInvalid.isErr()) {
|
||||||
|
expect(someInvalid.err).toBe('Invalid JSON: invalid');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with Result.from and Result.partition together', () => {
|
||||||
|
// Arrange
|
||||||
|
const inputs = ['1', 'invalid', '3', 'also invalid'];
|
||||||
|
const results = inputs.map(s =>
|
||||||
|
Result.from(() => parseInt(s), () => `Bad: ${s}`)
|
||||||
|
).map((r): Result<number, string> =>
|
||||||
|
(r as ResultInterface<number, string>).flatMap((n): Result<number, string> => isNaN(n) ? err(`NaN from parse`) : ok(n))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const partitioned = Result.partition(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(partitioned.values).toEqual([1, 3]);
|
||||||
|
expect(partitioned.errors).toEqual(['NaN from parse', 'NaN from parse']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
208
src/result/spec/namespace.type-spec.ts
Normal file
208
src/result/spec/namespace.type-spec.ts
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Result, Ok, Err, ok, err, ResultPartitionResult } from "../";
|
||||||
|
|
||||||
|
describe("Result namespace - Type Tests", () => {
|
||||||
|
describe("Result.from", () => {
|
||||||
|
it("should infer Ok type from function return type", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const numberResult = Result.from(() => 42);
|
||||||
|
const stringResult = Result.from(() => "hello");
|
||||||
|
const objectResult = Result.from(() => ({ id: 1 }));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberResult).toEqualTypeOf<Result<number, unknown>>();
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<Result<string, unknown>>();
|
||||||
|
expectTypeOf(objectResult).toEqualTypeOf<Result<{ id: number }, unknown>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should infer error type from errorMap", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const stringErrResult = Result.from(() => 42, () => "error");
|
||||||
|
const numberErrResult = Result.from(() => 42, () => 404);
|
||||||
|
const objectErrResult = Result.from(() => 42, () => ({ code: 500 }));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringErrResult).toEqualTypeOf<Result<number, string>>();
|
||||||
|
expectTypeOf(numberErrResult).toEqualTypeOf<Result<number, number>>();
|
||||||
|
expectTypeOf(objectErrResult).toEqualTypeOf<Result<number, { code: number }>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept unknown error type in errorMap parameter", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const result = Result.from(
|
||||||
|
() => 42,
|
||||||
|
(error: unknown) => `Error: ${error}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Result<number, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject async functions with a type-level error", () => {
|
||||||
|
// @ts-expect-error — async functions should not be passed to Result.from
|
||||||
|
const result = Result.from(async () => 42);
|
||||||
|
|
||||||
|
expectTypeOf(result).toBeString();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject async functions even with errorMap", () => {
|
||||||
|
// @ts-expect-error — async functions should not be passed to Result.from
|
||||||
|
const result = Result.from(async () => 42, () => "error");
|
||||||
|
|
||||||
|
expectTypeOf(result).toBeString();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Result.isResult", () => {
|
||||||
|
it("should act as type guard narrowing to Result<unknown, unknown>", () => {
|
||||||
|
// Arrange
|
||||||
|
const value: unknown = ok(42);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (Result.isResult(value)) {
|
||||||
|
expectTypeOf(value).toEqualTypeOf<Result<unknown, unknown>>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept any value", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(Result.isResult).toBeCallableWith(42);
|
||||||
|
expectTypeOf(Result.isResult).toBeCallableWith("hello");
|
||||||
|
expectTypeOf(Result.isResult).toBeCallableWith(null);
|
||||||
|
expectTypeOf(Result.isResult).toBeCallableWith(undefined);
|
||||||
|
expectTypeOf(Result.isResult).toBeCallableWith(ok(42));
|
||||||
|
expectTypeOf(Result.isResult).toBeCallableWith(err("error"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return boolean", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const result = Result.isResult(ok(42));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<boolean>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Result.all", () => {
|
||||||
|
it("should return Result<T[], E> for Result<T, E>[]", () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [ok(1), ok(2)] as Result<number, string>[];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const allResult = Result.all(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(allResult).toEqualTypeOf<Result<number[], string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex types", () => {
|
||||||
|
// Arrange
|
||||||
|
type User = { id: number; name: string };
|
||||||
|
const results: Result<User, Error>[] = [] as Result<User, Error>[];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const allResult = Result.all(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(allResult).toEqualTypeOf<Result<User[], Error>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Ok<T[]> when all inputs are known Ok", () => {
|
||||||
|
const allResult = Result.all([ok(1), ok(2), ok(3)]);
|
||||||
|
expectTypeOf(allResult).toEqualTypeOf<Ok<number[]>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Result.partition", () => {
|
||||||
|
it("should return ResultPartitionResult<T, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [] as Result<number, string>[];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const partitioned = Result.partition(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(partitioned).toEqualTypeOf<ResultPartitionResult<number, string>>();
|
||||||
|
expectTypeOf(partitioned.values).toEqualTypeOf<number[]>();
|
||||||
|
expectTypeOf(partitioned.errors).toEqualTypeOf<string[]>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex types", () => {
|
||||||
|
// Arrange
|
||||||
|
type User = { id: number; name: string };
|
||||||
|
type ApiError = { code: number; message: string };
|
||||||
|
const results: Result<User, ApiError>[] = [] as Result<User, ApiError>[];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const partitioned = Result.partition(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(partitioned).toEqualTypeOf<ResultPartitionResult<User, ApiError>>();
|
||||||
|
expectTypeOf(partitioned.values).toEqualTypeOf<User[]>();
|
||||||
|
expectTypeOf(partitioned.errors).toEqualTypeOf<ApiError[]>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Result.compact", () => {
|
||||||
|
it("should return T[] for Result<T, E>[]", () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [] as Result<number, string>[];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const compacted = Result.compact(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(compacted).toEqualTypeOf<number[]>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex types", () => {
|
||||||
|
// Arrange
|
||||||
|
type User = { id: number; name: string };
|
||||||
|
const results: Result<User, string>[] = [] as Result<User, string>[];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const compacted = Result.compact(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(compacted).toEqualTypeOf<User[]>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Result.some", () => {
|
||||||
|
it("should return boolean", () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [] as Result<number, string>[];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.some(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<boolean>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept Result<T, E>[]", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(Result.some).toBeCallableWith([] as Result<number, string>[]);
|
||||||
|
expectTypeOf(Result.some).toBeCallableWith([ok(1)] as Result<number, string>[]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Result.every", () => {
|
||||||
|
it("should return boolean", () => {
|
||||||
|
// Arrange
|
||||||
|
const results: Result<number, string>[] = [] as Result<number, string>[];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Result.every(results);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<boolean>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept Result<T, E>[]", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(Result.every).toBeCallableWith([] as Result<number, string>[]);
|
||||||
|
expectTypeOf(Result.every).toBeCallableWith([ok(1)] as Result<number, string>[]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
190
src/result/spec/nullable.spec.ts
Normal file
190
src/result/spec/nullable.spec.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Result, Ok, Err, ok, err } from '../';
|
||||||
|
|
||||||
|
describe('Result toNullable/toUndefined - Runtime Tests', () => {
|
||||||
|
describe('toNullable method', () => {
|
||||||
|
describe('when Result is Ok', () => {
|
||||||
|
it('should return the contained value', () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ok(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const value = result.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(value).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return various types of values', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(ok(42).toNullable()).toBe(42);
|
||||||
|
expect(ok('hello').toNullable()).toBe('hello');
|
||||||
|
expect(ok(true).toNullable()).toBe(true);
|
||||||
|
expect(ok({ id: 1 }).toNullable()).toEqual({ id: 1 });
|
||||||
|
expect(ok([1, 2, 3]).toNullable()).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return falsy values correctly', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(ok(0).toNullable()).toBe(0);
|
||||||
|
expect(ok(false).toNullable()).toBe(false);
|
||||||
|
expect(ok('').toNullable()).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the exact same reference for objects', () => {
|
||||||
|
// Arrange
|
||||||
|
const obj = { name: 'test', data: [1, 2, 3] };
|
||||||
|
const result = ok(obj);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const value = result.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(value).toBe(obj);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Result is Err', () => {
|
||||||
|
it('should return null', () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<number, string> = err('error') as Result<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const value = result.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(value).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null regardless of error type', () => {
|
||||||
|
// Arrange
|
||||||
|
const stringErr: Result<number, string> = err('error') as Result<number, string>;
|
||||||
|
const numberErr: Result<number, number> = err(404) as Result<number, number>;
|
||||||
|
const objectErr: Result<number, { code: number }> = err({ code: 500 }) as Result<number, { code: number }>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(stringErr.toNullable()).toBeNull();
|
||||||
|
expect(numberErr.toNullable()).toBeNull();
|
||||||
|
expect(objectErr.toNullable()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toUndefined method', () => {
|
||||||
|
describe('when Result is Ok', () => {
|
||||||
|
it('should return the contained value', () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ok(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const value = result.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(value).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return various types of values', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(ok(42).toUndefined()).toBe(42);
|
||||||
|
expect(ok('hello').toUndefined()).toBe('hello');
|
||||||
|
expect(ok(true).toUndefined()).toBe(true);
|
||||||
|
expect(ok({ id: 1 }).toUndefined()).toEqual({ id: 1 });
|
||||||
|
expect(ok([1, 2, 3]).toUndefined()).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return falsy values correctly', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(ok(0).toUndefined()).toBe(0);
|
||||||
|
expect(ok(false).toUndefined()).toBe(false);
|
||||||
|
expect(ok('').toUndefined()).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the exact same reference for objects', () => {
|
||||||
|
// Arrange
|
||||||
|
const obj = { name: 'test', data: [1, 2, 3] };
|
||||||
|
const result = ok(obj);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const value = result.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(value).toBe(obj);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when Result is Err', () => {
|
||||||
|
it('should return undefined', () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<number, string> = err('error') as Result<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const value = result.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(value).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined regardless of error type', () => {
|
||||||
|
// Arrange
|
||||||
|
const stringErr: Result<number, string> = err('error') as Result<number, string>;
|
||||||
|
const numberErr: Result<number, number> = err(404) as Result<number, number>;
|
||||||
|
const objectErr: Result<number, { code: number }> = err({ code: 500 }) as Result<number, { code: number }>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(stringErr.toUndefined()).toBeUndefined();
|
||||||
|
expect(numberErr.toUndefined()).toBeUndefined();
|
||||||
|
expect(objectErr.toUndefined()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toNullable and toUndefined integration', () => {
|
||||||
|
it('should behave consistently for Ok values', () => {
|
||||||
|
// Arrange
|
||||||
|
const values = [42, 'hello', true, { id: 1 }, [1, 2, 3], 0, false, ''];
|
||||||
|
|
||||||
|
values.forEach(value => {
|
||||||
|
// Act
|
||||||
|
const nullable = ok(value).toNullable();
|
||||||
|
const undef = ok(value).toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(nullable).toBe(value);
|
||||||
|
expect(undef).toBe(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should behave consistently for Err values', () => {
|
||||||
|
// Arrange
|
||||||
|
const errors = ['error', 404, new Error('test'), { code: 500 }];
|
||||||
|
|
||||||
|
errors.forEach(error => {
|
||||||
|
// Act
|
||||||
|
const nullable = (err(error) as Result<number, typeof error>).toNullable();
|
||||||
|
const undef = (err(error) as Result<number, typeof error>).toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(nullable).toBeNull();
|
||||||
|
expect(undef).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work after map/flatMap chains', () => {
|
||||||
|
// Arrange
|
||||||
|
const okResult: Result<number, string> = ok(5) as Result<number, string>;
|
||||||
|
const errResult: Result<number, string> = err('error') as Result<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const okNullable = okResult.map(x => x * 2).toNullable();
|
||||||
|
const okUndefined = okResult.map(x => x * 2).toUndefined();
|
||||||
|
const errNullable = errResult.map(x => x * 2).toNullable();
|
||||||
|
const errUndefined = errResult.map(x => x * 2).toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(okNullable).toBe(10);
|
||||||
|
expect(okUndefined).toBe(10);
|
||||||
|
expect(errNullable).toBeNull();
|
||||||
|
expect(errUndefined).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
151
src/result/spec/nullable.type-spec.ts
Normal file
151
src/result/spec/nullable.type-spec.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Result, ResultInterface, Ok, Err, ok, err } from "../";
|
||||||
|
|
||||||
|
// Helper to use the interface signatures which correctly unify T and E
|
||||||
|
const ri = <T, E>(r: Result<T, E>) => r as unknown as ResultInterface<T, E>;
|
||||||
|
|
||||||
|
describe("Result toNullable/toUndefined - Type Tests", () => {
|
||||||
|
describe("toNullable method", () => {
|
||||||
|
it("should return T for Ok<T>", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberOk = ok(42);
|
||||||
|
const stringOk = ok("hello");
|
||||||
|
const objectOk = ok({ id: 1, name: "test" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberValue = numberOk.toNullable();
|
||||||
|
const stringValue = stringOk.toNullable();
|
||||||
|
const objectValue = objectOk.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberValue).toEqualTypeOf<number>();
|
||||||
|
expectTypeOf(stringValue).toEqualTypeOf<string>();
|
||||||
|
expectTypeOf(objectValue).toEqualTypeOf<{ id: number; name: string }>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null for Err<E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const stringErr = err("error");
|
||||||
|
const numberErr = err(404);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const nullFromString = stringErr.toNullable();
|
||||||
|
const nullFromNumber = numberErr.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(nullFromString).toEqualTypeOf<null>();
|
||||||
|
expectTypeOf(nullFromNumber).toEqualTypeOf<null>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return T | null for Result<T, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<number, string> = ok(42) as Result<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const value = result.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(value).toEqualTypeOf<number | null>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex types", () => {
|
||||||
|
// Arrange
|
||||||
|
type ComplexType = { data: Array<{ id: number }> };
|
||||||
|
const result: Result<ComplexType, string> = ok({ data: [{ id: 1 }] }) as Result<ComplexType, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const value = result.toNullable();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(value).toEqualTypeOf<ComplexType | null>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toUndefined method", () => {
|
||||||
|
it("should return T for Ok<T>", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberOk = ok(42);
|
||||||
|
const stringOk = ok("hello");
|
||||||
|
const objectOk = ok({ id: 1, name: "test" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberValue = numberOk.toUndefined();
|
||||||
|
const stringValue = stringOk.toUndefined();
|
||||||
|
const objectValue = objectOk.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberValue).toEqualTypeOf<number>();
|
||||||
|
expectTypeOf(stringValue).toEqualTypeOf<string>();
|
||||||
|
expectTypeOf(objectValue).toEqualTypeOf<{ id: number; name: string }>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined for Err<E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const stringErr = err("error");
|
||||||
|
const numberErr = err(404);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const undefFromString = stringErr.toUndefined();
|
||||||
|
const undefFromNumber = numberErr.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(undefFromString).toEqualTypeOf<undefined>();
|
||||||
|
expectTypeOf(undefFromNumber).toEqualTypeOf<undefined>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return T | undefined for Result<T, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<number, string> = ok(42) as Result<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const value = result.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(value).toEqualTypeOf<number | undefined>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex types", () => {
|
||||||
|
// Arrange
|
||||||
|
type ComplexType = { data: Array<{ id: number }> };
|
||||||
|
const result: Result<ComplexType, string> = ok({ data: [{ id: 1 }] }) as Result<ComplexType, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const value = result.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(value).toEqualTypeOf<ComplexType | undefined>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toNullable and toUndefined after transformations", () => {
|
||||||
|
it("should have correct types after map", () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<number, string> = ok(42) as Result<number, string>;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const mappedNullable = result.map(x => x.toString()).toNullable();
|
||||||
|
const mappedUndefined = result.map(x => x.toString()).toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(mappedNullable).toEqualTypeOf<string | null>();
|
||||||
|
expectTypeOf(mappedUndefined).toEqualTypeOf<string | undefined>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have correct types after flatMap", () => {
|
||||||
|
// Arrange
|
||||||
|
const result = ri<number, string>(ok(42) as Result<number, string>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const flatMappedNullable = result
|
||||||
|
.flatMap(x => ok(x.toString()) as Result<string, string>)
|
||||||
|
.toNullable();
|
||||||
|
const flatMappedUndefined = result
|
||||||
|
.flatMap(x => ok(x.toString()) as Result<string, string>)
|
||||||
|
.toUndefined();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(flatMappedNullable).toEqualTypeOf<string | null>();
|
||||||
|
expectTypeOf(flatMappedUndefined).toEqualTypeOf<string | undefined>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
496
src/result/spec/promise.spec.ts
Normal file
496
src/result/spec/promise.spec.ts
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Result, ResultPromise, ok, err } from '../';
|
||||||
|
|
||||||
|
describe('ResultPromise namespace - Runtime Tests', () => {
|
||||||
|
describe('ResultPromise.from', () => {
|
||||||
|
it('should return Ok when async function resolves', async () => {
|
||||||
|
// Arrange
|
||||||
|
const fn = () => Promise.resolve(42);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await ResultPromise.from(fn);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value).toBe(42);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Err when async function rejects', async () => {
|
||||||
|
// Arrange
|
||||||
|
const error = new Error('async error');
|
||||||
|
const fn = () => Promise.reject(error);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await ResultPromise.from(fn);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply errorMap when async function rejects and errorMap is provided', async () => {
|
||||||
|
// Arrange
|
||||||
|
const fn = () => Promise.reject(new Error('original'));
|
||||||
|
const errorMap = (e: unknown) => `Mapped: ${(e as Error).message}`;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await ResultPromise.from(fn, errorMap);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('Mapped: original');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call errorMap when async function succeeds', async () => {
|
||||||
|
// Arrange
|
||||||
|
const fn = () => Promise.resolve(42);
|
||||||
|
const errorMapSpy = vi.fn((e: unknown) => `Mapped: ${e}`);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await ResultPromise.from(fn, errorMapSpy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
expect(errorMapSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle async functions returning falsy values', async () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const zeroResult = await ResultPromise.from(() => Promise.resolve(0));
|
||||||
|
const falseResult = await ResultPromise.from(() => Promise.resolve(false));
|
||||||
|
const emptyResult = await ResultPromise.from(() => Promise.resolve(''));
|
||||||
|
const nullResult = await ResultPromise.from(() => Promise.resolve(null));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(zeroResult.isOk()).toBe(true);
|
||||||
|
expect(falseResult.isOk()).toBe(true);
|
||||||
|
expect(emptyResult.isOk()).toBe(true);
|
||||||
|
expect(nullResult.isOk()).toBe(true);
|
||||||
|
|
||||||
|
if (zeroResult.isOk()) expect(zeroResult.value).toBe(0);
|
||||||
|
if (falseResult.isOk()) expect(falseResult.value).toBe(false);
|
||||||
|
if (emptyResult.isOk()) expect(emptyResult.value).toBe('');
|
||||||
|
if (nullResult.isOk()) expect(nullResult.value).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle thrown errors (not just rejections)', async () => {
|
||||||
|
// Arrange
|
||||||
|
const fn = async () => {
|
||||||
|
throw new Error('thrown');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await ResultPromise.from(fn);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect((result.err as Error).message).toBe('thrown');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ResultPromise.all', () => {
|
||||||
|
it('should return Ok with all values when all promises resolve to Ok', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(ok(1) as Result<number, string>),
|
||||||
|
Promise.resolve(ok(2) as Result<number, string>),
|
||||||
|
Promise.resolve(ok(3) as Result<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await ResultPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value).toEqual([1, 2, 3]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the first Err when any promise resolves to Err', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(ok(1) as Result<number, string>),
|
||||||
|
Promise.resolve(err('first error') as Result<number, string>),
|
||||||
|
Promise.resolve(ok(3) as Result<number, string>),
|
||||||
|
Promise.resolve(err('second error') as Result<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await ResultPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
if (result.isErr()) {
|
||||||
|
expect(result.err).toBe('first error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Ok with empty array for empty input', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Result<number, string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await ResultPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value).toEqual([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject if any input promise rejects', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(ok(1) as Result<number, string>),
|
||||||
|
Promise.reject(new Error('promise rejected')),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(ResultPromise.all(promises)).rejects.toThrow('promise rejected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ResultPromise.partition', () => {
|
||||||
|
it('should partition resolved results into values and errors', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(ok(1) as Result<number, string>),
|
||||||
|
Promise.resolve(err('error1') as Result<number, string>),
|
||||||
|
Promise.resolve(ok(3) as Result<number, string>),
|
||||||
|
Promise.resolve(err('error2') as Result<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const partitioned = await ResultPromise.partition(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(partitioned.values).toEqual([1, 3]);
|
||||||
|
expect(partitioned.errors).toEqual(['error1', 'error2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all Ok results', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(ok(1) as Result<number, string>),
|
||||||
|
Promise.resolve(ok(2) as Result<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const partitioned = await ResultPromise.partition(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(partitioned.values).toEqual([1, 2]);
|
||||||
|
expect(partitioned.errors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all Err results', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(err('error1') as Result<number, string>),
|
||||||
|
Promise.resolve(err('error2') as Result<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const partitioned = await ResultPromise.partition(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(partitioned.values).toEqual([]);
|
||||||
|
expect(partitioned.errors).toEqual(['error1', 'error2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty array', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Result<number, string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const partitioned = await ResultPromise.partition(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(partitioned.values).toEqual([]);
|
||||||
|
expect(partitioned.errors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject if any input promise rejects', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(ok(1) as Result<number, string>),
|
||||||
|
Promise.reject(new Error('promise rejected')),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(ResultPromise.partition(promises)).rejects.toThrow('promise rejected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ResultPromise.compact', () => {
|
||||||
|
it('should extract values from Ok instances', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(ok(1) as Result<number, string>),
|
||||||
|
Promise.resolve(err('error') as Result<number, string>),
|
||||||
|
Promise.resolve(ok(3) as Result<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const values = await ResultPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(values).toEqual([1, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array when all are Err', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(err('error1') as Result<number, string>),
|
||||||
|
Promise.resolve(err('error2') as Result<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const values = await ResultPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(values).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all values when all are Ok', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(ok(1) as Result<number, string>),
|
||||||
|
Promise.resolve(ok(2) as Result<number, string>),
|
||||||
|
Promise.resolve(ok(3) as Result<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const values = await ResultPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(values).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty array', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Result<number, string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const values = await ResultPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(values).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle falsy Ok values', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(ok(0) as Result<number | boolean | string, string>),
|
||||||
|
Promise.resolve(ok(false) as Result<number | boolean | string, string>),
|
||||||
|
Promise.resolve(ok('') as Result<number | boolean | string, string>),
|
||||||
|
Promise.resolve(err('error') as Result<number | boolean | string, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const values = await ResultPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(values).toEqual([0, false, '']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject if any input promise rejects', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(ok(1) as Result<number, string>),
|
||||||
|
Promise.reject(new Error('promise rejected')),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(ResultPromise.compact(promises)).rejects.toThrow('promise rejected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ResultPromise.some', () => {
|
||||||
|
it('should return true when at least one promise resolves to Ok', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(ok(1) as Result<number, string>),
|
||||||
|
Promise.resolve(err('error') as Result<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await ResultPromise.some(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when all promises resolve to Err', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(err('error1') as Result<number, string>),
|
||||||
|
Promise.resolve(err('error2') as Result<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await ResultPromise.some(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for empty array', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Result<number, string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await ResultPromise.some(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when all promises resolve to Ok', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(ok(1) as Result<number, string>),
|
||||||
|
Promise.resolve(ok(2) as Result<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await ResultPromise.some(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject if any input promise rejects', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(ok(1) as Result<number, string>),
|
||||||
|
Promise.reject(new Error('promise rejected')),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(ResultPromise.some(promises)).rejects.toThrow('promise rejected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ResultPromise.every', () => {
|
||||||
|
it('should return true when all promises resolve to Ok', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(ok(1) as Result<number, string>),
|
||||||
|
Promise.resolve(ok(2) as Result<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await ResultPromise.every(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when any promise resolves to Err', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(ok(1) as Result<number, string>),
|
||||||
|
Promise.resolve(err('error') as Result<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await ResultPromise.every(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for empty array', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Result<number, string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await ResultPromise.every(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when all promises resolve to Err', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(err('error1') as Result<number, string>),
|
||||||
|
Promise.resolve(err('error2') as Result<number, string>),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await ResultPromise.every(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject if any input promise rejects', async () => {
|
||||||
|
// Arrange
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve(ok(1) as Result<number, string>),
|
||||||
|
Promise.reject(new Error('promise rejected')),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(ResultPromise.every(promises)).rejects.toThrow('promise rejected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Integration tests', () => {
|
||||||
|
it('should work with ResultPromise.from and ResultPromise.all together', async () => {
|
||||||
|
// Arrange
|
||||||
|
const fetchValue = (n: number) =>
|
||||||
|
ResultPromise.from(() => Promise.resolve(n * 2), () => 'fetch error');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const allResult = await ResultPromise.all([
|
||||||
|
fetchValue(1),
|
||||||
|
fetchValue(2),
|
||||||
|
fetchValue(3),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(allResult.isOk()).toBe(true);
|
||||||
|
if (allResult.isOk()) {
|
||||||
|
expect(allResult.value).toEqual([2, 4, 6]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with ResultPromise.from and ResultPromise.partition together', async () => {
|
||||||
|
// Arrange
|
||||||
|
const tryParse = (s: string) =>
|
||||||
|
ResultPromise.from(
|
||||||
|
async () => {
|
||||||
|
const n = parseInt(s);
|
||||||
|
if (isNaN(n)) throw new Error('NaN');
|
||||||
|
return n;
|
||||||
|
},
|
||||||
|
() => `Invalid: ${s}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const partitioned = await ResultPromise.partition([
|
||||||
|
tryParse('1'),
|
||||||
|
tryParse('invalid'),
|
||||||
|
tryParse('3'),
|
||||||
|
tryParse('also invalid'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(partitioned.values).toEqual([1, 3]);
|
||||||
|
expect(partitioned.errors).toEqual(['Invalid: invalid', 'Invalid: also invalid']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
157
src/result/spec/promise.type-spec.ts
Normal file
157
src/result/spec/promise.type-spec.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Result, ResultPromise, ResultPartitionResult, ok, err } from "../";
|
||||||
|
|
||||||
|
describe("ResultPromise namespace - Type Tests", () => {
|
||||||
|
describe("ResultPromise.from", () => {
|
||||||
|
it("should infer Ok type from async function return type", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const numberResult = ResultPromise.from(() => Promise.resolve(42));
|
||||||
|
const stringResult = ResultPromise.from(() => Promise.resolve("hello"));
|
||||||
|
const objectResult = ResultPromise.from(() => Promise.resolve({ id: 1 }));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberResult).toEqualTypeOf<Promise<Result<number, unknown>>>();
|
||||||
|
expectTypeOf(stringResult).toEqualTypeOf<Promise<Result<string, unknown>>>();
|
||||||
|
expectTypeOf(objectResult).toEqualTypeOf<Promise<Result<{ id: number }, unknown>>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should infer error type from errorMap", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const stringErrResult = ResultPromise.from(
|
||||||
|
() => Promise.resolve(42),
|
||||||
|
() => "error"
|
||||||
|
);
|
||||||
|
const numberErrResult = ResultPromise.from(
|
||||||
|
() => Promise.resolve(42),
|
||||||
|
() => 404
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringErrResult).toEqualTypeOf<Promise<Result<number, string>>>();
|
||||||
|
expectTypeOf(numberErrResult).toEqualTypeOf<Promise<Result<number, number>>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept unknown error type in errorMap parameter", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const result = ResultPromise.from(
|
||||||
|
() => Promise.resolve(42),
|
||||||
|
(error: unknown) => `Error: ${error}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<Result<number, string>>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ResultPromise.all", () => {
|
||||||
|
it("should return Promise<Result<T[], E>>", () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Result<number, string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const allResult = ResultPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(allResult).toEqualTypeOf<Promise<Result<number[], string>>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex types", () => {
|
||||||
|
// Arrange
|
||||||
|
type User = { id: number; name: string };
|
||||||
|
const promises: Promise<Result<User, Error>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const allResult = ResultPromise.all(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(allResult).toEqualTypeOf<Promise<Result<User[], Error>>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ResultPromise.partition", () => {
|
||||||
|
it("should return Promise<ResultPartitionResult<T, E>>", () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Result<number, string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const partitioned = ResultPromise.partition(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(partitioned).toEqualTypeOf<Promise<ResultPartitionResult<number, string>>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex types", () => {
|
||||||
|
// Arrange
|
||||||
|
type User = { id: number; name: string };
|
||||||
|
type ApiError = { code: number; message: string };
|
||||||
|
const promises: Promise<Result<User, ApiError>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const partitioned = ResultPromise.partition(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(partitioned).toEqualTypeOf<Promise<ResultPartitionResult<User, ApiError>>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ResultPromise.compact", () => {
|
||||||
|
it("should return Promise<T[]>", () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Result<number, string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const compacted = ResultPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(compacted).toEqualTypeOf<Promise<number[]>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex types", () => {
|
||||||
|
// Arrange
|
||||||
|
type User = { id: number; name: string };
|
||||||
|
const promises: Promise<Result<User, string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const compacted = ResultPromise.compact(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(compacted).toEqualTypeOf<Promise<User[]>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ResultPromise.some", () => {
|
||||||
|
it("should return Promise<boolean>", () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Result<number, string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = ResultPromise.some(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<boolean>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept Promise<Result<T, E>>[]", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(ResultPromise.some).toBeCallableWith([] as Promise<Result<number, string>>[]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ResultPromise.every", () => {
|
||||||
|
it("should return Promise<boolean>", () => {
|
||||||
|
// Arrange
|
||||||
|
const promises: Promise<Result<number, string>>[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = ResultPromise.every(promises);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Promise<boolean>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept Promise<Result<T, E>>[]", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(ResultPromise.every).toBeCallableWith([] as Promise<Result<number, string>>[]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
324
src/result/spec/result.spec.ts
Normal file
324
src/result/spec/result.spec.ts
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Result, Ok, Err, ok, err } from '../';
|
||||||
|
|
||||||
|
describe('Result constructors and factories - Runtime Tests', () => {
|
||||||
|
describe('Ok constructor', () => {
|
||||||
|
it('should create Ok instance with provided value', () => {
|
||||||
|
// Arrange
|
||||||
|
const value = 42;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = new Ok(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(Ok);
|
||||||
|
expect(result.value).toBe(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should store different types of values correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const numberValue = 42;
|
||||||
|
const stringValue = 'hello';
|
||||||
|
const objectValue = { id: 1, name: 'test' };
|
||||||
|
const arrayValue = [1, 2, 3];
|
||||||
|
const functionValue = (x: number) => x * 2;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const numberOk = new Ok(numberValue);
|
||||||
|
const stringOk = new Ok(stringValue);
|
||||||
|
const objectOk = new Ok(objectValue);
|
||||||
|
const arrayOk = new Ok(arrayValue);
|
||||||
|
const functionOk = new Ok(functionValue);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(numberOk.value).toBe(numberValue);
|
||||||
|
expect(stringOk.value).toBe(stringValue);
|
||||||
|
expect(objectOk.value).toBe(objectValue);
|
||||||
|
expect(arrayOk.value).toBe(arrayValue);
|
||||||
|
expect(functionOk.value).toBe(functionValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null and undefined values', () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const nullOk = new Ok(null);
|
||||||
|
const undefinedOk = new Ok(undefined);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(nullOk.value).toBeNull();
|
||||||
|
expect(undefinedOk.value).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should implement ResultInterface methods', () => {
|
||||||
|
// Arrange
|
||||||
|
const okInstance = new Ok(42);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(typeof okInstance.isOk).toBe('function');
|
||||||
|
expect(typeof okInstance.isErr).toBe('function');
|
||||||
|
expect(typeof okInstance.map).toBe('function');
|
||||||
|
expect(typeof okInstance.mapErr).toBe('function');
|
||||||
|
expect(typeof okInstance.flatMap).toBe('function');
|
||||||
|
expect(typeof okInstance.flatMapErr).toBe('function');
|
||||||
|
expect(typeof okInstance.match).toBe('function');
|
||||||
|
expect(typeof okInstance.filter).toBe('function');
|
||||||
|
expect(typeof okInstance.toNullable).toBe('function');
|
||||||
|
expect(typeof okInstance.toUndefined).toBe('function');
|
||||||
|
expect(typeof okInstance.toString).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly identify as Ok', () => {
|
||||||
|
// Arrange
|
||||||
|
const okInstance = new Ok(42);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(okInstance.isOk()).toBe(true);
|
||||||
|
expect(okInstance.isErr()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Err constructor', () => {
|
||||||
|
it('should create Err instance with provided error', () => {
|
||||||
|
// Arrange
|
||||||
|
const error = 'something went wrong';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = new Err(error);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(Err);
|
||||||
|
expect(result.err).toBe(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should store different types of errors correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const stringError = 'error message';
|
||||||
|
const numberError = 404;
|
||||||
|
const objectError = { code: 'NOT_FOUND', message: 'Not found' };
|
||||||
|
const errorInstance = new Error('Test error');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const stringErr = new Err(stringError);
|
||||||
|
const numberErr = new Err(numberError);
|
||||||
|
const objectErr = new Err(objectError);
|
||||||
|
const errorErr = new Err(errorInstance);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(stringErr.err).toBe(stringError);
|
||||||
|
expect(numberErr.err).toBe(numberError);
|
||||||
|
expect(objectErr.err).toBe(objectError);
|
||||||
|
expect(errorErr.err).toBe(errorInstance);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should implement ResultInterface methods', () => {
|
||||||
|
// Arrange
|
||||||
|
const errInstance = new Err('error');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(typeof errInstance.isOk).toBe('function');
|
||||||
|
expect(typeof errInstance.isErr).toBe('function');
|
||||||
|
expect(typeof errInstance.map).toBe('function');
|
||||||
|
expect(typeof errInstance.mapErr).toBe('function');
|
||||||
|
expect(typeof errInstance.flatMap).toBe('function');
|
||||||
|
expect(typeof errInstance.flatMapErr).toBe('function');
|
||||||
|
expect(typeof errInstance.match).toBe('function');
|
||||||
|
expect(typeof errInstance.filter).toBe('function');
|
||||||
|
expect(typeof errInstance.toNullable).toBe('function');
|
||||||
|
expect(typeof errInstance.toUndefined).toBe('function');
|
||||||
|
expect(typeof errInstance.toString).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly identify as Err', () => {
|
||||||
|
// Arrange
|
||||||
|
const errInstance = new Err('error');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(errInstance.isOk()).toBe(false);
|
||||||
|
expect(errInstance.isErr()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ok() factory function', () => {
|
||||||
|
it('should create Ok instance with provided value', () => {
|
||||||
|
// Arrange
|
||||||
|
const value = 42;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = ok(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(Ok);
|
||||||
|
expect(result.value).toBe(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with various data types', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
42,
|
||||||
|
'hello',
|
||||||
|
true,
|
||||||
|
{ id: 1, name: 'test' },
|
||||||
|
[1, 2, 3],
|
||||||
|
null,
|
||||||
|
undefined,
|
||||||
|
(x: number) => x * 2
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(testValue => {
|
||||||
|
// Act
|
||||||
|
const result = ok(testValue);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(Ok);
|
||||||
|
expect(result.value).toBe(testValue);
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
expect(result.isErr()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create new instances for each call', () => {
|
||||||
|
// Arrange
|
||||||
|
const value = 42;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const first = ok(value);
|
||||||
|
const second = ok(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(first).toBeInstanceOf(Ok);
|
||||||
|
expect(second).toBeInstanceOf(Ok);
|
||||||
|
expect(first).not.toBe(second);
|
||||||
|
expect(first.value).toBe(second.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('err() factory function', () => {
|
||||||
|
it('should create Err instance with provided error', () => {
|
||||||
|
// Arrange
|
||||||
|
const error = 'failed';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = err(error);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(Err);
|
||||||
|
expect(result.err).toBe(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with various error types', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCases = [
|
||||||
|
'error message',
|
||||||
|
404,
|
||||||
|
new Error('Test error'),
|
||||||
|
{ code: 'ERR', message: 'Something failed' },
|
||||||
|
null,
|
||||||
|
undefined,
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(testError => {
|
||||||
|
// Act
|
||||||
|
const result = err(testError);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeInstanceOf(Err);
|
||||||
|
expect(result.err).toBe(testError);
|
||||||
|
expect(result.isOk()).toBe(false);
|
||||||
|
expect(result.isErr()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create new instances for each call', () => {
|
||||||
|
// Arrange
|
||||||
|
const error = 'error';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const first = err(error);
|
||||||
|
const second = err(error);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(first).toBeInstanceOf(Err);
|
||||||
|
expect(second).toBeInstanceOf(Err);
|
||||||
|
expect(first).not.toBe(second);
|
||||||
|
expect(first.err).toBe(second.err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toString()', () => {
|
||||||
|
it('should return "Ok(...)" for Ok instances', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(ok(42).toString()).toBe('Ok(42)');
|
||||||
|
expect(ok('hello').toString()).toBe('Ok(hello)');
|
||||||
|
expect(ok(true).toString()).toBe('Ok(true)');
|
||||||
|
expect(ok(null).toString()).toBe('Ok(null)');
|
||||||
|
expect(ok(undefined).toString()).toBe('Ok(undefined)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "Err(...)" for Err instances', () => {
|
||||||
|
// Arrange & Act & Assert
|
||||||
|
expect(err('error').toString()).toBe('Err(error)');
|
||||||
|
expect(err(404).toString()).toBe('Err(404)');
|
||||||
|
expect(err(null).toString()).toBe('Err(null)');
|
||||||
|
expect(err(undefined).toString()).toBe('Err(undefined)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle object values in toString()', () => {
|
||||||
|
// Arrange
|
||||||
|
const obj = { id: 1 };
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(ok(obj).toString()).toBe('Ok([object Object])');
|
||||||
|
expect(err(obj).toString()).toBe('Err([object Object])');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Integration tests', () => {
|
||||||
|
it('should allow creating mixed arrays of Results', () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const results: Result<number, string>[] = [
|
||||||
|
ok(1) as Result<number, string>,
|
||||||
|
ok(2) as Result<number, string>,
|
||||||
|
err('error') as Result<number, string>,
|
||||||
|
ok(3) as Result<number, string>
|
||||||
|
];
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results).toHaveLength(4);
|
||||||
|
expect(results[0].isOk()).toBe(true);
|
||||||
|
expect(results[1].isOk()).toBe(true);
|
||||||
|
expect(results[2].isErr()).toBe(true);
|
||||||
|
expect(results[3].isOk()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable type narrowing in runtime', () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<string, number> = ok('hello') as Result<string, number>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result.isOk()) {
|
||||||
|
expect(result.value).toBe('hello');
|
||||||
|
expect(typeof result.value).toBe('string');
|
||||||
|
} else {
|
||||||
|
fail('Expected result to be Ok');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle edge cases correctly', () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const zeroOk = ok(0);
|
||||||
|
const falseOk = ok(false);
|
||||||
|
const emptyOk = ok('');
|
||||||
|
const emptyArrayOk = ok([]);
|
||||||
|
|
||||||
|
// Assert - All should be Ok instances even with "falsy" values
|
||||||
|
expect(zeroOk.isOk()).toBe(true);
|
||||||
|
expect(falseOk.isOk()).toBe(true);
|
||||||
|
expect(emptyOk.isOk()).toBe(true);
|
||||||
|
expect(emptyArrayOk.isOk()).toBe(true);
|
||||||
|
|
||||||
|
expect(zeroOk.value).toBe(0);
|
||||||
|
expect(falseOk.value).toBe(false);
|
||||||
|
expect(emptyOk.value).toBe('');
|
||||||
|
expect(emptyArrayOk.value).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
207
src/result/spec/result.type-spec.ts
Normal file
207
src/result/spec/result.type-spec.ts
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import { Result, Ok, Err, ok, err, ResultInterface } from "../";
|
||||||
|
|
||||||
|
describe("Result constructors and factories - Type Tests", () => {
|
||||||
|
describe("Ok constructor", () => {
|
||||||
|
it("should create Ok<T> with correct type inference", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const numberOk = new Ok(42);
|
||||||
|
const stringOk = new Ok("hello");
|
||||||
|
const objectOk = new Ok({ id: 1, name: "test" });
|
||||||
|
const arrayOk = new Ok([1, 2, 3]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberOk).toEqualTypeOf<Ok<number>>();
|
||||||
|
expectTypeOf(stringOk).toEqualTypeOf<Ok<string>>();
|
||||||
|
expectTypeOf(objectOk).toEqualTypeOf<Ok<{ id: number; name: string }>>();
|
||||||
|
expectTypeOf(arrayOk).toEqualTypeOf<Ok<number[]>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should implement ResultInterface with correct type parameters", () => {
|
||||||
|
// Arrange
|
||||||
|
const okPrimitive = new Ok(42);
|
||||||
|
const okObject = new Ok({ value: "test" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(okPrimitive).toMatchTypeOf<ResultInterface<number, never>>();
|
||||||
|
expectTypeOf(okObject).toMatchTypeOf<ResultInterface<{ value: string }, never>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be assignable to Result<T, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const numberOk = new Ok(42);
|
||||||
|
const stringOk = new Ok("hello");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberOk).toMatchTypeOf<Result<number, string>>();
|
||||||
|
expectTypeOf(stringOk).toMatchTypeOf<Result<string, number>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve complex generic types", () => {
|
||||||
|
// Arrange
|
||||||
|
type ComplexType = { data: Array<{ id: number; tags: string[] }> };
|
||||||
|
const complexValue: ComplexType = { data: [{ id: 1, tags: ["a", "b"] }] };
|
||||||
|
const complexOk = new Ok(complexValue);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(complexOk).toEqualTypeOf<Ok<ComplexType>>();
|
||||||
|
expectTypeOf(complexOk).toMatchTypeOf<Result<ComplexType, any>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle nullable and undefined types correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
const nullableOk = new Ok(null);
|
||||||
|
const undefinedOk = new Ok(undefined);
|
||||||
|
const unionOk = new Ok("hello" as string | null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(nullableOk).toEqualTypeOf<Ok<null>>();
|
||||||
|
expectTypeOf(undefinedOk).toEqualTypeOf<Ok<undefined>>();
|
||||||
|
expectTypeOf(unionOk).toEqualTypeOf<Ok<string | null>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Err constructor", () => {
|
||||||
|
it("should create Err<E> with correct type inference", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const stringErr = new Err("error");
|
||||||
|
const numberErr = new Err(404);
|
||||||
|
const objectErr = new Err({ code: "NOT_FOUND", message: "Not found" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringErr).toEqualTypeOf<Err<string>>();
|
||||||
|
expectTypeOf(numberErr).toEqualTypeOf<Err<number>>();
|
||||||
|
expectTypeOf(objectErr).toEqualTypeOf<Err<{ code: string; message: string }>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should implement ResultInterface with correct type parameters", () => {
|
||||||
|
// Arrange
|
||||||
|
const errInstance = new Err("error");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(errInstance).toMatchTypeOf<ResultInterface<never, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be assignable to Result<T, E> for any T", () => {
|
||||||
|
// Arrange
|
||||||
|
const errInstance = new Err("error");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(errInstance).toMatchTypeOf<Result<number, string>>();
|
||||||
|
expectTypeOf(errInstance).toMatchTypeOf<Result<any, string>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have .err property with correct type", () => {
|
||||||
|
// Arrange
|
||||||
|
const stringErr = new Err("error");
|
||||||
|
const objectErr = new Err({ code: 500, message: "Internal error" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringErr.err).toEqualTypeOf<string>();
|
||||||
|
expectTypeOf(objectErr.err).toEqualTypeOf<{ code: number; message: string }>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ok() factory function", () => {
|
||||||
|
it("should infer correct return type from value", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const numberOk = ok(42);
|
||||||
|
const stringOk = ok("hello");
|
||||||
|
const booleanOk = ok(true);
|
||||||
|
const objectOk = ok({ id: 1 });
|
||||||
|
const arrayOk = ok([1, 2, 3]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(numberOk).toEqualTypeOf<Ok<number>>();
|
||||||
|
expectTypeOf(stringOk).toEqualTypeOf<Ok<string>>();
|
||||||
|
expectTypeOf(booleanOk).toEqualTypeOf<Ok<boolean>>();
|
||||||
|
expectTypeOf(objectOk).toEqualTypeOf<Ok<{ id: number }>>();
|
||||||
|
expectTypeOf(arrayOk).toEqualTypeOf<Ok<number[]>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve literal types", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const literalNumber = ok(42 as const);
|
||||||
|
const literalString = ok("hello" as const);
|
||||||
|
const literalObject = ok({ type: "user" } as const);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(literalNumber).toEqualTypeOf<Ok<42>>();
|
||||||
|
expectTypeOf(literalString).toEqualTypeOf<Ok<"hello">>();
|
||||||
|
expectTypeOf(literalObject).toEqualTypeOf<Ok<{ readonly type: "user" }>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle function types correctly", () => {
|
||||||
|
// Arrange
|
||||||
|
const simpleFunc = (x: number) => x * 2;
|
||||||
|
const asyncFunc = async (x: string) => x.toUpperCase();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const funcOk = ok(simpleFunc);
|
||||||
|
const asyncFuncOk = ok(asyncFunc);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(funcOk).toEqualTypeOf<Ok<(x: number) => number>>();
|
||||||
|
expectTypeOf(asyncFuncOk).toEqualTypeOf<Ok<(x: string) => Promise<string>>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with any type", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(ok).toBeCallableWith(42);
|
||||||
|
expectTypeOf(ok).toBeCallableWith("string");
|
||||||
|
expectTypeOf(ok).toBeCallableWith(null);
|
||||||
|
expectTypeOf(ok).toBeCallableWith(undefined);
|
||||||
|
expectTypeOf(ok).toBeCallableWith({ complex: { nested: "object" } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("err() factory function", () => {
|
||||||
|
it("should infer correct return type from error", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const stringErr = err("error");
|
||||||
|
const numberErr = err(404);
|
||||||
|
const objectErr = err({ code: "ERR", message: "failed" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(stringErr).toEqualTypeOf<Err<string>>();
|
||||||
|
expectTypeOf(numberErr).toEqualTypeOf<Err<number>>();
|
||||||
|
expectTypeOf(objectErr).toEqualTypeOf<Err<{ code: string; message: string }>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable with any type", () => {
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(err).toBeCallableWith("error");
|
||||||
|
expectTypeOf(err).toBeCallableWith(404);
|
||||||
|
expectTypeOf(err).toBeCallableWith(new Error("test"));
|
||||||
|
expectTypeOf(err).toBeCallableWith(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be assignable to any Result<T, E>", () => {
|
||||||
|
// Arrange
|
||||||
|
const errResult = err("error");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(errResult).toMatchTypeOf<Result<number, string>>();
|
||||||
|
expectTypeOf(errResult).toMatchTypeOf<Result<string, string>>();
|
||||||
|
expectTypeOf(errResult).toMatchTypeOf<Result<{ any: "object" }, string>>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Type narrowing with constructors", () => {
|
||||||
|
it("should enable proper type narrowing with isOk/isErr", () => {
|
||||||
|
// Arrange
|
||||||
|
const result: Result<number, string> = ok(42) as Result<number, string>;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (result.isOk()) {
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Ok<number>>();
|
||||||
|
expectTypeOf(result.value).toEqualTypeOf<number>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isErr()) {
|
||||||
|
expectTypeOf(result).toEqualTypeOf<Err<string>>();
|
||||||
|
expectTypeOf(result.err).toEqualTypeOf<string>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
5
src/util/index.ts
Normal file
5
src/util/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* Checks if a value is nullish (null or undefined).
|
||||||
|
*/
|
||||||
|
export const isNullish = (value: unknown): value is null | undefined =>
|
||||||
|
value === null || value === undefined;
|
||||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2022",
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["es2022"],
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist", "src/**/*.spec.ts", "src/**/*.type-spec.ts", "src/**/*.test.ts"]
|
||||||
|
}
|
||||||
9
tsconfig.lib.json
Normal file
9
tsconfig.lib.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["src/**/*.spec.ts", "src/**/*.d-spec.ts", "src/**/*.test.ts", "src/**/*.infer.ts"]
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user