nalloc

中文文档
0.5.61发布于 2026/11/04MITTypeScriptESM + CJS零依赖构建来源已验证

Rust-like Option and Result for TypeScript with near-zero allocations for extreme performance

原始使用文档
文档目录28 节
  1. Why nalloc?
  2. Benchmarks
  3. Install
  4. Quick start
  5. How it works
  6. Coming from other libraries?
  7. From neverthrow
  8. From fp-ts
  9. From oxide.ts
  10. From ts-results
  11. From Rust
  12. Option
  13. Result
  14. Iter
  15. NonEmpty
  16. API Reference
  17. Option
  18. Result
  19. Schema
  20. HTTP
  21. Iter
  22. Utilities
  23. ESLint plugin
  24. Codemod: migrate from neverthrow
  25. Comparison
  26. Alternatives
  27. License
  28. Contributing

nalloc

Rust-like safety at JavaScript speed.

Type-safe Option and Result for TypeScript with near-zero allocations for extreme performance.

npm version npm downloads

Why nalloc?

Most Option/Result libraries wrap every value in an object. nalloc doesn't.

Other libraries:    Some(42) --> { _tag: 'Some', value: 42 }   // allocates
nalloc:            Some(42) --> 42                            // zero allocation

Other libraries:    Ok(data) --> { _tag: 'Ok', value: data }   // allocates
nalloc:            Ok(data) --> data                          // zero allocation
  • Rust-like types - Option<T> and Result<T, E> with full pattern matching and combinators
  • Near-zero allocations - Some and Ok are the values themselves. Only Err allocates.
  • Extreme performance - Up to 2x faster than alternatives on the happy path

Benchmarks

Operationnallocneverthrowts-resultsoxide.ts
ok()91M ops/s75M ops/s56M ops/s40M ops/s
err()55M ops/s52M ops/s0.1M ops/s47M ops/s
some()85M ops/s-40M ops/s50M ops/s
Result.map74M ops/s53M ops/s47M ops/s64M ops/s
Option.map71M ops/s-52M ops/s41M ops/s

Run pnpm bench to reproduce.

Install

npm install nalloc

Quick start

import { Option, Result, ok, err, none, pipe, gen } from 'nalloc';

// Option: the value IS the Option
const port = Option.fromNullable(process.env.PORT);
const portNum = Option.unwrapOr(port, '3000');

// Result: Ok is the value itself, Err wraps the error
const config = Result.tryCatch(
  () => JSON.parse(raw),
  () => 'invalid json',
);
const data = Result.match(
  config,
  (cfg) => cfg,
  (error) => ({ fallback: true }),
);

// gen: Rust-like ? operator with type-safe errors
const result = gen(function* ($) {
  const a = yield* $(parseNumber('10'));
  const b = yield* $(parseNumber('5'));
  return a + b;
}); // Result<number, ParseError>

// pipe: left-to-right composition
const userId = pipe(
  Result.tryCatch(() => JSON.parse(raw)),
  (r) => Result.map(r, (data) => data.userId),
  (r) => Result.unwrapOr(r, 0),
);

How it works

TypeRepresentationAllocation
Option<T>T | null | undefinedZero
Some<T>The value itselfZero
Nonenull or undefinedZero
Ok<T>The value itself (branded)Zero
Err<E>Minimal wrapper { error: E }One object

This means zero GC pressure on the happy path - your success values stay as plain values.

Coming from other libraries?

From neverthrow

// neverthrow
import { ok, err, Result } from 'neverthrow';
const result: Result<number, string> = ok(42);
result.map((x) => x * 2);

// nalloc - same concepts, faster execution
import { Result, ok, err } from 'nalloc';
const result = ok(42); // zero allocation
Result.map(result, (x) => x * 2); // function-based API

From fp-ts

// fp-ts
import { pipe } from 'fp-ts/function';
import * as O from 'fp-ts/Option';
pipe(
  O.some(42),
  O.map((x) => x * 2),
);

// nalloc - simpler, faster
import { Option, pipe } from 'nalloc';
pipe(42, (v) => Option.map(v, (x) => x * 2)); // built-in pipe, value IS the Option

From oxide.ts

// oxide.ts
import { Some, None, Ok, Err } from 'oxide.ts';
const opt = Some(42);
opt.map((x) => x * 2);
const result = Ok(42);
result.mapErr((e) => new Error(e));

// nalloc - no wrapper objects on the happy path
import { Option, Result, ok } from 'nalloc';
Option.map(42, (x) => x * 2); // 42 is the Option itself
const result = ok(42); // zero allocation
Result.mapErr(result, (e) => new Error(e));

From ts-results

// ts-results
import { Ok, Err, Some, None } from 'ts-results';
const result = new Ok(42);
result.map((x) => x * 2);
const opt = Some(42);
opt.unwrapOr(0);

// nalloc - same safety, zero allocations
import { Option, Result, ok } from 'nalloc';
const result = ok(42); // no wrapper
Result.map(result, (x) => x * 2);
Option.unwrapOr(42, 0); // value IS the Option

From Rust

The API mirrors Rust's Option and Result:

import { Option, Result, ok, err, none, gen } from 'nalloc';

// Rust: Some(42).map(|x| x * 2)
Option.map(42, (x) => x * 2);

// Rust: Ok(42).and_then(|x| if x > 0 { Ok(x) } else { Err("negative") })
Result.andThen(ok(42), (x) => (x > 0 ? ok(x) : err('negative')));

// Rust: result.unwrap_or(0)
Result.unwrapOr(result, 0);

// Rust: let value = get_value()?
const value = gen(function* ($) {
  const a = yield* $(getValue());
  return a + 1;
});

Option

An Option<T> is T | null | undefined. The value itself is the Option.

import { Option, none } from 'nalloc';

// Create from nullable
const maybePort = Option.fromNullable(process.env.PORT);

// Transform
const doubled = Option.map(maybePort, (p) => parseInt(p) * 2);
const validated = Option.filter(doubled, (n) => n > 0);

// Extract
const port = Option.unwrapOr(maybePort, '3000');

// Pattern match
const label = Option.match(
  maybePort,
  (value) => `Port: ${value}`,
  () => 'Port: default',
);

// Assert and narrow
Option.assertSome(maybePort, 'PORT is required');

// Filter collections
const activeIds = Option.filterMap(users, (user) => (user.isActive ? user.id : none));

// Async
const maybeUser = await Option.fromPromise(fetchUserById(id));

Result

A Result<T, E> is either Ok<T> (the value itself) or Err<E> (a wrapper).

import { Result, Schema, ok, err, gen, genAsync, pipe } from 'nalloc';

// Wrap throwing functions
const parsed = Result.tryCatch(
  () => JSON.parse(raw),
  (e) => 'invalid json',
);

// gen: Rust-like ? operator with preserved error types
const result = gen(function* ($) {
  const config = yield* $(parseConfig(raw));
  const db = yield* $(connectDb(config.url));
  return db.query('SELECT 1');
}); // Result<QueryResult, ConfigError | DbError>

// Async gen
const data = await genAsync(async function* ($) {
  const res = yield* $(await Result.fromPromise(fetchUser(id)));
  const posts = yield* $(await Result.fromPromise(fetchPosts(res.id)));
  return { user: res, posts };
}); // Promise<Result<{user, posts}, unknown>>

// wrap / toThrowable: ecosystem boundaries
const safeParse = Result.wrap(JSON.parse); // throwing -> Result
safeParse('{"a":1}'); // Ok({a: 1})
const throwingFind = Result.toThrowable(findUser); // Result -> throwing
throwingFind('123'); // returns User or throws

// Standard Schema validation (Zod, Valibot, ArkType, etc.)
const validated = Schema.fromSchema(userSchema, input);
// Result<User, readonly SchemaIssue[]>

// wrapSchema: bind a schema once, reuse the validator
const parseUser = Schema.wrapSchema(userSchema);

// Transform with pipe
const userId = pipe(
  parsed,
  (r) => Result.map(r, (data) => data.userId),
  (r) => Result.flatMap(r, (id) => (id > 0 ? ok(id) : err('invalid id'))),
);

// Pattern match
const user = Result.match(
  parsed,
  (data) => data.user,
  (error) => null,
);

// Assert and narrow
Result.assertOk(loaded, 'Config required');

// Collections
const combined = Result.all([ok(1), ok(2), ok(3)]); // Ok([1, 2, 3])
const first = Result.any([err('a'), ok(42), err('b')]); // Ok(42)
const [oks, errs] = Result.partition(results);

Iter

Iterator utilities for working with fallible iteration and Result streams.

import { Iter } from 'nalloc';

// Safe iteration: wraps throws as Err, values as Ok
for (const result of Iter.safeIter(riskyIterator)) {
  if (isOk(result)) handleValue(result);
  else handleError(result.error);
}

// mapWhile: yields mapped values while fn returns Some
const taken = [...Iter.mapWhile([1, 2, 3, 4, 5], (n) => (n < 4 ? n * 10 : null))]; // [10, 20, 30]

// tryCollect: collect Results into Result<T[], E>
const collected = Iter.tryCollect(results); // Ok([1, 2, 3]) or first Err

// tryFold: fold with early exit on Err
const sum = Iter.tryFold([1, 2, 3], 0, (acc, n) => ok(acc + n));

// tryForEach: iterate with early exit on Err
Iter.tryForEach(items, (item) => processItem(item));

NonEmpty

Proves at the type level that an array has at least one element. The runtime value is a plain array - no wrapper, no class, no cloning. fromArray returns the original array value when it is non-empty.

import { NonEmpty, Option } from 'nalloc';

NonEmpty.isNonEmpty([]); // false
NonEmpty.isNonEmpty([1]); // true, narrows to ReadonlyNonEmptyArray<number>

const values: readonly number[] = loadValues();
if (NonEmpty.isNonEmpty(values)) {
  const first: number = values[0]; // typed, no undefined
}

// fromArray returns None for [] and Some(values) otherwise
const opt = NonEmpty.fromArray(values); // Option<ReadonlyNonEmptyArray<number>>

Two Result combinators now carry the non-empty guarantee in their error type:

  • Result.collectAll(results) returns Result<T[], NonEmptyArray<E>> - if the result is Err, the error array is guaranteed to contain at least one entry.
  • Result.any(results) returns Result<Widen<T>, NonEmptyArray<WidenNever<E>>> for the same reason: any only fails when every input failed.

API Reference

Option

FunctionDescription
fromNullable(value)Convert nullable to Option
fromPromise(promise)Promise to Option (rejection = None)
map(opt, fn)Transform Some value
flatMap(opt, fn)Chain Option-returning functions
andThen(opt, fn)Alias for flatMap
tap(opt, fn)Side effect on Some, return original
tapNone(opt, fn)Side effect on None, return original
filter(opt, predicate)Keep Some if predicate passes
match(opt, onSome, onNone)Pattern match
unwrap(opt)Extract or throw
unwrapOr(opt, default)Extract or default
unwrapOrElse(opt, fn)Extract or compute default
unwrapOrReturn(opt, fn)Extract or return computed value
expect(opt, msg)Extract or throw with message
assertSome(opt, msg?)Assert and narrow to Some
isSome(opt) / isNone(opt)Type guards
isSomeAnd(opt, pred)Some and predicate passes
isNoneOr(opt, pred)None or predicate passes
or(opt, other)Return first Some
orElse(opt, fn)Return Some or compute fallback
and(opt, other)Return second if first is Some
xor(opt, other)Some if exactly one is Some
zip(opt, other)Combine two Options into tuple
unzip(opt)Split tuple Option into two
flatten(opt)Flatten nested Option
contains(opt, value)Check if Some contains value
mapOr(opt, default, fn)Map or return default
mapOrElse(opt, defaultFn, fn)Map or compute default
toArray(opt)Some to [value], None to []
toNullable(opt)Some to value, None to null
toUndefined(opt)Some to value, None to undefined
okOr(opt, error)Option to Result
okOrElse(opt, fn)Option to Result with computed error
ofOk(result)Ok to Some, Err to None
ofErr(result)Err to Some, Ok to None
filterMap(items, fn)Map and filter in one pass
findMap(items, fn)Find first Some from mapping

Result

FunctionDescription
tryCatch(fn, onError?)Wrap throwing function
tryCatchMaybePromise(fn, onError?)Wrap sync-or-async, preserving sync
of(fn)Alias for tryCatch (no error mapper)
wrap(fn, onError?)Wrap throwing function once, reuse
toThrowable(fn)Inverse of wrap: Result-returning to throwing
gen(fn)Generator do-notation with typed errors
genAsync(fn)Async generator do-notation
safeTry(fn)Imperative error handling with unwrap
safeTryAsync(fn)Async version of safeTry
unwrap(result)Extract Ok or throw error value
unwrapErr(result)Extract Err or throw
unwrapOr(result, default)Extract Ok or default
unwrapOrElse(result, fn)Extract Ok or compute from error
unwrapOrReturn(result, fn)Extract Ok or return computed value
expect(result, msg)Extract Ok or throw with message
expectErr(result, msg)Extract Err or throw with message
map(result, fn)Transform Ok value
mapErr(result, fn)Transform Err value
bimap(result, okFn, errFn)Transform both Ok and Err
flatMap(result, fn)Chain Result-returning functions
andThen(result, fn)Alias for flatMap
tap(result, fn)Side effect on Ok, return original
tapErr(result, fn)Side effect on Err, return original
match(result, onOk, onErr)Pattern match
assertOk(result, msg?)Assert and narrow to Ok
assertErr(result, msg?)Assert and narrow to Err
isOk(result) / isErr(result)Type guards
isOkAnd(result, pred)Ok and predicate passes
isErrAnd(result, pred)Err and predicate passes
isSomeErr(result)Err with non-null error
and(result, other)Return second if first is Ok
or(result, other)Return first Ok
orElse(result, fn)Ok or compute fallback from error
zip(a, b)Combine two Ok into tuple
zipWith(a, b, fn)Combine two Ok with function
flatten(result)Flatten nested Result
transpose(result)Result
toOption(result)Ok to Some, Err to None
toErrorOption(result)Err to Some, Ok to None
mapOr(result, default, fn)Map Ok or return default
mapOrElse(result, defaultFn, fn)Map Ok or compute default
all(results)Collect all Ok or first Err
any(results)First Ok or all Errs
collect(results)Collect Ok values or first Err
collectAll(results)All Ok or all Errs
partition(results)Split into [oks, errs]
partitionAsync(promises)Async partition
filterOk(results)Extract all Ok values
filterErr(results)Extract all Err values
settleMaybePromise(values)Settle sync/async values to Results
partitionMaybePromise(values)Partition sync/async Results

Schema

Standard Schema v1 validation. Tree-shakeable: import the Schema namespace from nalloc, or directly from nalloc/schema.

FunctionDescription
fromSchema(schema, value)Validate via Standard Schema v1, returns Result
wrapSchema(schema)Bind a schema once, returns a reusable validator

HTTP

Optional nalloc/http subpath. Works in any runtime with fetch (browser, Node 18+, Bun, Deno, edge workers); kept out of the main entry so the core stays free of environment-typed globals.

Native fetch resolves successfully on 4xx/5xx - only transport failures reject. So the generic helpers alone let an HTTP error flow down the Ok path looking like success:

const r = await Result.fromPromise(fetch(url)); // Ok(Response { status: 404 }) - not an Err!

fromFetch closes that gap: Ok means the request connected AND returned 2xx, and every failure mode lands in the error channel, typed:

import { fromFetch } from 'nalloc/http';

const res = await fromFetch(url);
// Result<Response, Response | TypeError | DOMException>
// Ok(Response)      -> connected and 2xx
// Err(Response)     -> reached the server, non-2xx (read status/body from it)
// Err(TypeError)    -> network/CORS failure
// Err(DOMException) -> aborted or timed out

Already holding a Response (custom fetch wrapper, middleware, a different HTTP client)? fromResponse is the status check alone, and composes with fromPromise if you need the two steps separately:

import { fromResponse } from 'nalloc/http';

const checked = fromResponse(response); // Ok(response) if response.ok, else Err(response)
FunctionDescription
fromFetch(input, init?)Run fetch: Ok(response) for 2xx, Err on non-2xx or transport failure
fromResponse(response)Ok(response) if response.ok, else Err(response)

Iter

FunctionDescription
safeIter(source)Wrap iterable: values as Ok, throws as Err
mapWhile(source, fn)Yield mapped values while fn returns Some
tryCollect(source)Collect Results into Result<T[], E>
tryFold(source, init, fn)Fold with early exit on Err
tryForEach(source, fn)Iterate with early exit on Err

Utilities

FunctionDescription
pipe(value, ...fns)Thread value through functions left-to-right

ESLint plugin

nalloc ships an optional ESLint plugin (flat config) at nalloc/eslint. It is dev-time only and never enters your runtime bundle. The typescript-eslint toolchain is an optional peer - install it alongside ESLint:

npm install -D eslint @typescript-eslint/parser @typescript-eslint/utils typescript
// eslint.config.js
import nalloc from 'nalloc/eslint';
import tsParser from '@typescript-eslint/parser';

export default [
  {
    files: ['**/*.ts'],
    languageOptions: {
      parser: tsParser,
      parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
    },
    plugins: { nalloc },
    rules: {
      'nalloc/must-use': 'error',
      'nalloc/no-unwrap': 'error',
    },
  },
  { files: ['**/*.test.ts'], rules: { 'nalloc/no-unwrap': 'off' } },
];
RuleWhat it flagsType info
nalloc/must-useA Result/Option value used as a bare statement and discardedRequired (parserOptions.projectService)
nalloc/no-unwrapunwrap / unwrapErr / expect / expectErr, which throw on failureNot required

nalloc.configs.recommended turns both rules on; it does not configure the parser, so a type-aware setup for must-use is still your config's responsibility. Without type info, must-use reports the standard typescript-eslint parserServices error while no-unwrap keeps working.

Codemod: migrate from neverthrow

nalloc bundles a best-effort codemod that rewrites neverthrow code to nalloc. It needs the optional oxc-parser peer (dev-time only, never in your runtime bundle):

npm install -D oxc-parser
npx nalloc-codemod src --report migration-report.md   # add --dry to preview

What it converts: the neverthrow import (aliases preserved); method calls to function calls (r.map(f) -> Result.map(r, f), .andThen -> Result.flatMap, .mapErr, .orElse, .match, .unwrapOr, .isOk/.isErr, .andTee/.orTee -> tap/tapErr, _unsafeUnwrap -> Result.unwrap); chains of two or more methods fold into pipe(...); fromThrowable -> Result.wrap; combine -> Result.all; combineWithAllErrors -> Result.collectAll; directly-awaited fromPromise -> Result.fromPromise; Result<T, E> type annotations -> ResultType<T, E>. Edits are text splices, so untouched code keeps its formatting byte for byte.

What it refuses and reports instead: ResultAsync and everything that produces one (okAsync, errAsync, chained fromPromise, asyncMap, ...) - the nalloc idiom is genAsync, which is a manual rewrite; safeTry generators (use gen); namespace imports (import * as nt). Those keep a residual neverthrow import so the file still compiles mid-migration.

Two safety properties to know. Conversion is provenance-based: ambiguous method names like .map are only rewritten when the receiver provably holds a Result (constructed from ok/err/combine/ a fromThrowable-wrapped function, annotated with a neverthrow type, or chained with neverthrow-only methods) - a bare .map on an unknown receiver is assumed to be an array and left alone. And migrate the whole project in one run: converted call sites assume their values are nalloc values, which only holds once the constructors producing them are converted too.

Comparison

Featurenallocneverthrowfp-tsoxide.tsts-results
Zero-alloc OptionYesNoNoNoNo
Zero-alloc OkYesNoNoNoNo
Bundle sizeTinySmallLargeSmallSmall
Learning curveLowLowHighLowLow
Async supportYesYesYesLimitedNo
Tree-shakeableYesYesYesYesYes
Iterator utilitiesYesNoYesNoNo
gen (? operator)YesYesNoNoNo
pipeYesNoYesNoNo
Schema validationYesNoNoNoNo
Ecosystem interopYesNoNoNoNo

Alternatives

Looking for a TypeScript Result/Option library? Here's how nalloc compares:

  • neverthrow - Popular, method-chaining API. Allocates a wrapper for every Ok and Err. nalloc avoids allocation on the happy path entirely.
  • fp-ts / effect - Full functional programming ecosystem with Either and Option. Powerful but heavy and steep learning curve. nalloc focuses on Result/Option with a simpler API.
  • oxide.ts - Rust-inspired with class-based wrappers. Every Some/Ok allocates an object. nalloc represents Some/Ok as the raw value.
  • ts-results / ts-results-es - Direct Rust port with class instances. Similar API surface to nalloc, but allocates on every construction.
  • true-myth - Result and Maybe with a functional API. Wraps all values. nalloc is a similar philosophy with zero-allocation design.

nalloc is designed for codebases where allocation pressure matters - high-throughput servers, hot loops, and performance-sensitive paths - while keeping the same safety guarantees.

License

MIT

Contributing

Contributions welcome! Open an issue or PR for ideas, bug fixes, or docs.

版本与发布记录

nalloc 共发布 49 个版本,首次发布于 2026/01/23;过去 12 个月发布了 49 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.5.61latest2026/11/04今天
0.5.602026/11/031 天前
0.5.592026/10/314 天前
0.5.582026/10/287 天前
0.5.572026/10/278 天前
0.5.562026/10/2312 天前

关于 nalloc 的常见问题

nalloc 是什么?

Rust-like Option and Result for TypeScript with near-zero allocations for extreme performance

如何安装 nalloc?

在项目目录运行 npm install nalloc,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 nalloc-codemod,可以用 npx nalloc-codemod 直接运行。

nalloc 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

nalloc 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

nalloc 可以商用吗?

nalloc 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

nalloc 还在维护吗?

最新版本 0.5.61 发布于 2026/11/04(今天);至今共发布 49 个版本,其中过去 12 个月发布了 49 个;首次发布于 2026/01/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。