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

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