Skip to content

Commit 21f3a8c

Browse files
egeozcanclaude
andcommitted
Add arity checking for type-safe function calls
- Add ExactArityFn<Fn, N> helper type to enforce exact parameter count - Update all placeholder overloads (1-4 args) to use arity checking - Functions with wrong arity now produce compile-time 'never' type errors - Placeholder positions use Awaited<T> for lambda contextual typing - Add type-level test file with @ts-expect-error validation - Add typecheck:arity npm script - Document arity system in CLAUDE.md and README.md - Bump version to 3.2.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent edff768 commit 21f3a8c

File tree

9 files changed

+399
-81
lines changed

9 files changed

+399
-81
lines changed

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [3.2.0] - 2025-01-23
6+
7+
### Added
8+
9+
- **Arity checking**: Functions now receive compile-time errors when passed extra arguments
10+
- `ppipe(8).pipe(subtract, _, 3, 5, 10)` now errors if `subtract` only takes 2 params
11+
- Uses `ExactArityFn<Fn, N>` helper type to enforce exact parameter count
12+
- Variadic functions (rest params) are allowed through the check
13+
- Type-level test file (`test/types.test.ts`) with `@ts-expect-error` validation
14+
- New npm script `typecheck:arity` for running type-level tests
15+
16+
### Changed
17+
18+
- Placeholder overloads now use `Awaited<T>` for contextual typing of lambda parameters
19+
- Overloads use `ReturnType<Fn>` instead of generic `R` for better type extraction
20+
21+
### Documentation
22+
23+
- Updated CLAUDE.md with arity checking system design
24+
- Updated README.md with type safety strengths and limitations
25+
526
## [3.1.0] - 2025-01-21
627

728
### Added

CLAUDE.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,28 @@ All TypeScript escape hatches are disabled via ESLint:
6161
- `createPipe` async/sync branch returns
6262

6363
**IMPORTANT: Never add any new type assertions, `any` types, ts-ignore comments, or other TypeScript escape hatches. The 4 existing boundary assertions are the maximum allowed. If new code cannot be written without escape hatches, restructure the approach using type predicates, discriminated unions, or function overloads instead.**
64+
65+
### Arity Checking System
66+
67+
The type system enforces that functions receive the correct number of arguments. This prevents silent bugs where extra arguments are passed to functions that don't expect them.
68+
69+
**How it works:**
70+
71+
1. **`ExactArityFn<Fn, N>` helper type** - Only matches functions with exactly N parameters (or variadic functions). When a function has wrong arity, the type becomes `never` and the overload doesn't match.
72+
73+
2. **Explicit overloads (1-4 args)** - Each overload uses `ExactArityFn<Fn, N>` to ensure the function's parameter count matches the number of arguments passed.
74+
75+
3. **Variadic fallback (5+ args)** - Uses `VariadicPipeResult` which checks `FinalArgs<Args, T> extends Parameters<Fn>`. Returns `never` on mismatch.
76+
77+
**Example of caught error:**
78+
```typescript
79+
const subtract = (a: number, b: number) => a - b; // 2 params
80+
ppipe(8).pipe(subtract, _, 3, 5, 10); // 4 args - type error!
81+
// Result is 'never', accessing .value produces: "Property 'value' does not exist on type 'never'"
82+
```
83+
84+
**Design tradeoffs:**
85+
- Error appears when accessing `.value`, not at the `.pipe()` call site (TypeScript limitation)
86+
- Untyped lambdas work for 1-4 args due to contextual typing from `Awaited<T>` in placeholder positions
87+
- 5+ args require typed functions/lambdas because variadic fallback can't provide contextual typing
88+
- Variadic functions (`...rest` params) are allowed through the arity check

README.md

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ These features relied on Proxy magic that returned `any` types, breaking TypeScr
237237

238238
## Type Safety
239239

240-
ppipe v3.x provides complete type inference:
240+
ppipe v3.x provides complete type inference with **arity checking** - passing extra arguments to functions that don't expect them produces compile-time errors:
241241

242242
```typescript
243243
// Types are inferred correctly through the chain
@@ -267,6 +267,50 @@ const debugPipe = ppipe.extend({
267267
debugPipe(5).log().pipe(x => x * 2).value; // x is number, result is number
268268
```
269269

270+
### Arity Checking
271+
272+
Functions are checked to ensure they receive the correct number of arguments:
273+
274+
```typescript
275+
const subtract = (a: number, b: number) => a - b;
276+
277+
// ✓ Correct - 2-param function with 2 args
278+
ppipe(10).pipe(subtract, _, 3).value; // 7
279+
280+
// ✗ Error - 2-param function with 4 args
281+
ppipe(10).pipe(subtract, _, 3, 5, 10).value;
282+
// Type error: Property 'value' does not exist on type 'never'
283+
```
284+
285+
### Type Safety Strengths
286+
287+
- **Full inference for lambdas** - Untyped lambdas get correct types for 1-4 arguments
288+
- **Arity mismatch detection** - Extra arguments to named functions produce compile errors
289+
- **Async tracking** - The type system tracks whether the chain contains any async operations
290+
- **Extension type preservation** - Generic identity extensions (like `log`) preserve the pipe's type
291+
- **No `any` in public API** - Complete type safety throughout
292+
293+
### Type Safety Limitations
294+
295+
| Scenario | Behavior |
296+
|----------|----------|
297+
| 5+ args with untyped lambda | Requires explicit type annotations |
298+
| Arity errors | Appear on `.value` access, not at `.pipe()` call |
299+
| Variadic functions | Allowed through arity check (by design) |
300+
301+
```typescript
302+
// 5+ args needs type annotations
303+
ppipe(1).pipe(
304+
(a: number, b: string, c: boolean, d: number, e: string) => a + d,
305+
_, "x", true, 4, "end"
306+
).value; // ✓ Works
307+
308+
ppipe(1).pipe(
309+
(a, b, c, d, e) => a, // ✗ 'a' will be 'never' - use typed lambda
310+
_, "x", true, 4, "end"
311+
);
312+
```
313+
270314
## Testing
271315

272316
100% test coverage is maintained. To run tests:

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ export default tseslint.config(
233233
},
234234
},
235235
{
236-
ignores: ["dist/", "dist-test/", "node_modules/"],
236+
ignores: ["dist/", "dist-test/", "node_modules/", "test/types.test.ts"],
237237
},
238238
{
239239
// Test files can use type assertions for testing edge cases

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ppipe",
3-
"version": "3.1.0",
3+
"version": "3.2.0",
44
"description": "Strictly-typed piping without the operator support",
55
"type": "module",
66
"main": "./dist/index.js",
@@ -22,6 +22,7 @@
2222
"prebuild": "npm run clean",
2323
"prepublishOnly": "npm run build",
2424
"typecheck": "tsc --noEmit",
25+
"typecheck:arity": "tsc test/types.test.ts --noEmit --strict --module esnext --moduleResolution node",
2526
"test": "npm run build:test && c8 --include='dist-test/src/**/*.js' --reporter=text --reporter=html mocha dist-test/test/test.js",
2627
"coverage": "c8 check-coverage --include='dist-test/src/**/*.js' --lines 100 --functions 100 --branches 100 --statements 100",
2728
"lint": "eslint --fix src/ test/",

0 commit comments

Comments
 (0)