All notable changes to this project will be documented in this file.
Type-level release. No runtime behaviour changes — all 55 runtime tests pass unmodified at 100% coverage. Several types became stricter, so this is a major bump.
- Extensions are no longer offered on pipes they cannot accept.
ExtensionMethodsinferred the extension's first parameter but never checked it against the pipe's value type, so every extension appeared on every pipe.ppipe.extend({ upper: (v: string) => v.toUpperCase() })(42).upper()type-checked cleanly and threwv.toUpperCase is not a functionat runtime. Now guarded by an assignability probe against the pipe's value type. Generic pass-through extensions (<G>(v: G) => G) accept any value and are unaffected .value/.valresolve to an exact type. They were alwaysT | Promise<T>, so the correctly trackedAsyncflag never reached callers and every call site needed a cast. Now typedPipeValue<T, Async>—Tfor sync chains,Promise<T>for async onesextend()now overrides same-named extensions instead of intersecting them. The type wasE & NewEwhile the runtime spread replaced the key; nowMergeExtensions<E, NewE>IsAsynctestsPromiseLike, notPromise. The runtime awaits anything with a callable.then(isThenable), so a bare thenable was classified sync and.valuetyped as a plain value while returning a Promise:const n: number = ppipe(somePromiseLike).valuecompiled, thenn.toFixed()threwIsAsync<never>is nowfalseinstead ofnever, which leaked into.valueasPromise<never>. Distribution over genuine unions is retained on purpose:IsAsync<number | Promise<number>>staysboolean, so.valuestaysnumber | Promise<number>— the honest answer when the compiler cannot tell which it is- Correlated generic extensions are no longer mistaken for identity functions.
<G>(v: G, replacement: G) => GinfersVandRasunknownexactly as a true identity function does, so the pipe's type was preserved andppipe.extend({ replace })(1).replace("wrong")on a number pipe typed asnumberwhile returning a string.ExtensionResultTypenow also inspects the trailing argument types. Extensions with concretely typed extra parameters —<G>(v: G, label?: string) => G— are unaffected - The extension guard no longer rejects overloaded extension functions. It is now an
assignability probe rather than a comparison against the inferred first parameter, because
inferonly ever sees an overloaded function's last signature. An extension declared(v: string): string/(v: number): numberwas unusable on a string pipe ppipe(promise).valueno longer resolves to the uninhabitablePromise<T> | Promise<Promise<T>>- Fixed a typo in the 4-argument
{0,2}placeholder overload, wherebanddwere forced to share one type parameter. The overload was dead — such calls silently fell through to the variadic fallback
- Arity mismatches now produce
ArityMismatch<Message>instead of a barenever, so the error readsProperty 'value' does not exist on type 'ArityMismatch<"the arguments passed to .pipe() do not match the piped function's parameters">'. The error still surfaces on member access, not at the.pipe()call typecheck:arityrenamed totypecheck:typesand now runs as part ofnpm test— the type-level suite previously ran only when invoked by handtest/types.test.tsrewritten aroundExpect<Equals<A, B>>assertions. The previous version usedascasts (const t1Check: number = t1.value as number), which asserted nothing; it would have passed even if.valuewereany. Now covers exact result types, async propagation, extension guards, override semantics, and the documented overload limitation
- Exported types
PipeValue,MergeExtensions,ArityMismatch tsconfig.types-test.jsonso the type-level suite inherits the project's full strictness. It overridesexclude, which is inherited throughextendsand filtersinclude— without that the suite silently checks nothing
IsFunction<T>— exported but never used
- README: exact
.valuetyping, extension value-type checking,extend()override semantics, and a new limitations section covering overloaded functions (with a lambda-wrapping workaround) and the deliberate acceptance of zero-parameter functions - CLAUDE.md: extension type rules, why
Asyncmust stay load-bearing, whyIsAsyncis deliberately three-state (and must keep distributing over unions), whyExactArityFnmust keep returningnever, and that an arity mismatch is only reported on member access — never at the.pipe()call
- No new type assertions,
anytypes, orts-ignorecomments. The count of boundary assertions remains exactly 4, as required by CLAUDE.md
- Arity checking: Functions now receive compile-time errors when passed extra arguments
ppipe(8).pipe(subtract, _, 3, 5, 10)now errors ifsubtractonly takes 2 params- Uses
ExactArityFn<Fn, N>helper type to enforce exact parameter count - Variadic functions (rest params) are allowed through the check
- Type-level test file (
test/types.test.ts) with@ts-expect-errorvalidation - New npm script
typecheck:arityfor running type-level tests
- Placeholder overloads now use
Awaited<T>for contextual typing of lambda parameters - Overloads use
ReturnType<Fn>instead of genericRfor better type extraction
- Updated CLAUDE.md with arity checking system design
- Updated README.md with type safety strengths and limitations
- Generic pass-through extensions now preserve the pipe's type
- Extensions like
<T>(value: T) => Tcorrectly maintain type through the chain - Enables type-safe
log(),tap(), and similar utility extensions
- Extensions like
- Complete TypeScript rewrite with strict typing
- Full IDE autocomplete and type inference support
- Discriminated union state management for type-safe async/sync/error handling
- 100% test coverage on source files
- Strict ESLint rules disable all TypeScript escape hatches:
- No
anytypes - No type assertions (
as) - No ts-ignore/ts-expect-error comments
- No non-null assertions (
!)
- No
- Features that couldn't be strictly typed (see Migration section in README):
- Deep property access (
_.a.b.c) - Array spreading (
..._) - Direct method access (
.map(fn)) - Context binding (
.with(ctx)) - Callable syntax (
ppipe(val)(fn))
- Deep property access (
- Updated all dependencies to latest versions
See v2.x README for previous features.