initial commit

This commit is contained in:
2026-03-30 08:34:35 +02:00
commit 49d8ea67ba
103 changed files with 22387 additions and 0 deletions

39
.gitignore vendored Normal file
View 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

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
v24.13.1

5
.prettierrc Normal file
View File

@@ -0,0 +1,5 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120
}

62
CLAUDE.md Normal file
View 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
View 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
View 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.

View 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,
};

View 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;
}

View 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';

View 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);
});
}

View 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);
});

View 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);
});
}

View 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);
});

View 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);
});

View 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);
});

View 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);
});

View 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
View 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

File diff suppressed because it is too large Load Diff

View 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);
});
}

View 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);
});

View 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);
});
}

View 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);
});

View 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);
});

View 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);
});
}

View 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);
});

View 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);
});
}

View 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);
});

View 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);
});

View 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);
});

View 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);
});

View 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);
});

View 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);
});

View 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);
});

View 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);
});

View 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
View 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"]
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
benchmark: {
include: ['**/*.bench.ts'],
},
},
});

14
eslint.config.mjs Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

3
src/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './result';
export * from './option';
export * from './query';

533
src/option/index.ts Normal file
View 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;

View 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);
}
});
});
});

View 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
View 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');
}
});
});
});

View 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
View 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");
});
});
});

View 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>>();
});
});
});

View 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');
});
});
});

View 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>();
});
});
});

View 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);
});
});
});

View 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>[]);
});
});
});

View 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)');
});
});
});

View 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>();
});
});
});

View 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([]);
});
});
});

View 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>();
}
});
});
});

View 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);
});
});
});

View 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
View 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;

View 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);
}
});
});
});

View 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>>();
});
});
});

View 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();
});
});
});

View 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
View 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');
}
});
});
});

View 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
View 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);
});
});
});

View 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>>();
});
});
});

View 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');
});
});
});

View 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>();
});
});
});

View 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);
});
});
});

View 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>();
});
});
});

View 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();
});
});
});

View 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>();
});
});
});

View 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');
});
});
});

View 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>>();
});
});
});

View 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([]);
});
});
});

View 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
View 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;

View 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);
}
});
});
});

View 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>>();
});
});
});

View 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);
});
});
});

View 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
View 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');
}
});
});
});

View 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
View 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);
});
});
});

View 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>>();
});
});
});

View 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');
});
});
});

View 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>();
});
});
});

View 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']);
});
});
});

View 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>[]);
});
});
});

View 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();
});
});
});

View 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>();
});
});
});

View 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']);
});
});
});

View 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>>[]);
});
});
});

View 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([]);
});
});
});

View 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
View 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
View 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
View 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