@nunofyobiz/effect-extras
v3.2.0
Published
Generic, framework-agnostic extensions of the Effect standard library (ArrayX, OptionX, RecordX, StructX, …).
Maintainers
Readme
@nunofyobiz/effect-extras
Generic, framework-agnostic extensions of the Effect
standard library. These are the *X utility modules — ArrayX, OptionX,
RecordX, StructX, and friends — that extend Effect's own modules with small,
universal patterns used repeatedly across projects.
Each module is named after the Effect (or native) module it extends, suffixed
with X: ArrayX extends Array, OptionX extends Option, and so on. They
are pure, generic, and carry no domain or framework knowledge — that is the
whole point, and the bar every addition has to clear (see
What belongs here).
Requires Effect v4 (peer
effect@^4.0.0-beta.*). The publishedeffect.websitedocs still describe v3, which differs (e.g.ResultreplacedEither).
import {
ArrayX,
OptionX,
RecordX,
StructX,
nn,
} from "@nunofyobiz/effect-extras";Install
pnpm add @nunofyobiz/effect-extraseffect is a peer dependency — your project must already depend on a
compatible version of effect. This package extends Effect; it does not bundle
it.
Tree-shaking
The package is side-effect free ("sideEffects": false) and is built — the
same way Effect is — with tsc (one ESM file per
module, no bundling) plus Babel's
annotate-pure-calls
pass, which stamps every helper /*#__PURE__*/. A bundler therefore keeps only
what you actually use. Three import styles, finest-grained first:
// Subpath, named — only this function (and its real deps) reach your bundle.
import { compactNullable } from "@nunofyobiz/effect-extras/ArrayX";
// Subpath, namespace — `ArrayX.*`, tree-shaken per function (Effect-style).
import * as ArrayX from "@nunofyobiz/effect-extras/ArrayX";
// Root barrel — convenient; unused *modules* are shaken away, but a module you
// touch is kept whole (per-module granularity).
import { ArrayX } from "@nunofyobiz/effect-extras";Measured (minified + brotli, effect externalized): a single function lands at
~40 B, a whole module at ~1 kB, the entire library at ~4.4 kB — so a
subpath import pays for what it uses, not the library. Budgets are enforced in CI
via size-limit; packaging correctness via
publint.
What belongs here
This package has exactly one job: hold the generic *X helpers that extend
Effect with patterns worth reusing everywhere. The danger with a "utils"
package is scope creep, so the bar for adding something is deliberately high.
A utility belongs here only if all of these hold:
- It is not already in Effect. If
effect(or an@effect/*package) already does it, use that directly. Check the built-in modules first (Array,Option,Record,Predicate,String,Number,Order,Result,Match,Struct, …) — they're wide and well-tested, and most "manipulate this shape" needs already exist there. This package targets Effect v4 (beta); the Effect docs are v3, so verify any v4 signature against the installednode_modules/effecttypes. - It is generic and pure. It operates on type parameters (
<A>), has no side effects, no mutations, and would make sense in a project that shares nothing with yours. - It carries zero app knowledge. It never references a specific business
domain or data model (no
Project,User,Timeline, …), and never encodes product rules. Domain-shaped helpers live in the app that owns the domain — not here. This is the hard line. - If it is a thin wrapper around Effect built-ins, it earns its place. A small convenience wrapper is only worth adding when it is meaningfully useful and universal. If a one-liner at the call site is just as clear, don't wrap it — the indirection costs more than it saves.
Decision flowchart
flowchart TD
A([Candidate utility]) --> B{Does Effect already<br/>provide it?}
B -- Yes --> R1[/Use Effect directly —<br/>do not add it here/]
B -- No --> C{Does it encode any app's<br/>business logic or data model?}
C -- Yes --> R2[/Belongs in that app —<br/>this package is domain-free/]
C -- No --> D{Generic over <A>, pure,<br/>and reusable across<br/>unrelated projects?}
D -- No --> R2
D -- Yes --> E{Just a thin wrapper around<br/>an Effect built-in?}
E -- No --> OK([Add it: a real gap in<br/>Effect's surface])
E -- Yes --> F{Meaningfully useful<br/>AND universal?}
F -- No --> R3[/Skip it: a call-site<br/>one-liner is clearer/]
F -- Yes --> OKDoes NOT belong here
- Anything tied to a domain model, a database row, an API shape, or product copy.
- Anything that imports a framework (React, Next, a UI kit) or
node:*built-ins in a way that assumes a runtime — these helpers must work anywhere Effect works. - A wrapper that exists only to rename an Effect function, or to save a single obvious line. Reach for it at the call site instead.
- A control-flow combinator Effect already ships (
sequence,when,unless, …). Extend Effect's data surface, don't re-implement its control flow.
When you are unsure, leave it at the call site. A helper graduates into this package the moment a second, unrelated call site wants the same generic shape — not before.
Modules
Each module is exported as a namespace — from the package root and from a
matching subpath (@nunofyobiz/effect-extras/ArrayX); see
Tree-shaking:
| Module | Extends / purpose |
| -------------- | -------------------------------------------------------------------------- |
| ArrayX | Array helpers (grouping, ordered insertion) |
| BigIntX | BigInt helpers (toNumberOrThrow) |
| BooleanX | Boolean helpers |
| DurationX | Duration / DateTime diff helpers |
| EffectX | Effect bridges (flattenOption, fromOptionOrElse, tryUntil) |
| FormDataX | Schema-based FormData parsing |
| InclusiveOr | Inclusive-or of a left and/or right — terminology-free WarnResult |
| MapX | Native Map helpers |
| NonNullableX | Non-nullable assertions (fromNullableOrThrow, exported as nn) |
| NumberX | Number helpers |
| OptionX | Option helpers and rendering bridges |
| OrderX | Order helpers (rankedEnum) |
| PredicateX | Compound predicates (isNonEmptyString, matchRefine) |
| PromiseX | Promise helpers |
| RecordX | Record manipulation (modifyIfExists, upsert, collectBy) |
| ResultX | Result bridges (fromOption) |
| SchemaX | Effect Schema extensions (pick/omit/partial, branded strings) |
| SetX | Native Set helpers |
| StringX | String helpers |
| StructX | Conditional object-field construction (defined, filterDefined, some) |
| WarnResult | Inclusive-or result: a success value, warnings, or both |
Development
pnpm install
pnpm tc # typecheck (src + tests)
pnpm lint # ESLint (formatting included via eslint-plugin-prettier)
pnpm test # vitest run
pnpm build # emit dist/ (one ESM file + .d.ts per module): tsc + babel
pnpm publint # validate packaging (exports map, types, ESM) — needs a build
pnpm treeshake # enforce per-function tree-shaking budgets (size-limit) — needs a build
pnpm knip # unused code / deps
pnpm check-all # all of the above, in CI orderEvery public function needs exhaustive tests — every branch and edge case (empty, single-element, boundary), plus type-level correctness where the whole point of the helper is type narrowing. Generic utilities are consumed by every layer above them and have no domain context to specify them other than their tests, so the tests are the spec.
See AGENTS.md for the full contributor/agent guide (Effect
conventions, the *X module + barrel pattern, no as casts, commit/PR/release
workflow).
Releasing
This package uses Changesets.
- Add a changeset describing your change:
pnpm changeset. - Commit it and push/merge to
main. - The Release workflow opens a "Version Packages" PR. Merging it publishes to npm (with provenance) and creates a GitHub release.
