@nlozgachev/pipekit 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +40 -217
  2. package/package.json +2 -1
package/README.md CHANGED
@@ -4,250 +4,73 @@
4
4
 
5
5
  A TypeScript toolkit for writing code that means exactly what it says.
6
6
 
7
- ## What is this?
8
-
9
- A TypeScript toolkit for expressing uncertainty precisely — absent values, fallible operations,
10
- async workflows, loading states — with types that carry their own intent and operations that
11
- compose.
12
-
13
- Most TypeScript code encodes state as flags and nullable fields: `data | null`,
14
- `error: Error | null`, `loading: boolean`. This works at small scale, but it pushes the burden of
15
- knowing which combinations are valid onto every consumer. Nothing in the type system tells you that
16
- `data` is only meaningful when `loading` is false and `error` is null — you just have to know, and
17
- check.
18
-
19
- This library takes a different approach: represent the state space precisely, then provide a small,
20
- consistent set of operations to work with it. `Option` doesn't let you access a value that might not
21
- exist without deciding what to do when it doesn't. `Result` makes it impossible to forget the error
22
- case. `RemoteData` replaces a trio of booleans with four named, mutually exclusive states. Each type
23
- comes with a module of functions — constructors, transformations, extractors — that follow the same
24
- conventions (`map`, `chain`, `match`, `getOrElse`) and work with `pipe`.
25
-
26
- The consistency means knowledge transfers: once you know `Option`, picking up `Result` or
27
- `TaskResult` is mostly recognising the same operations in a new context. The composition means logic
28
- reads in the order it executes. And precise types mean the compiler tracks what's possible — so you
29
- don't have to.
7
+ ```sh
8
+ # npm / pnpm / yarn / bun
9
+ npm add @nlozgachev/pipekit
30
10
 
31
- ## Do I need to know functional programming to use this?
11
+ # Deno
12
+ deno add jsr:@nlozgachev/pipekit
13
+ ```
32
14
 
33
- No. The library avoids FP-specific jargon wherever possible. You won't find `Monad`, `Functor`, or
34
- `Applicative` in the API. Instead, you'll work with names that describe what they do:
15
+ ## What is this?
35
16
 
36
- - `Option` a value that might not exist
37
- - `Result` — an operation that can succeed or fail
38
- - `map` — transform a value inside a container
39
- - `chain` — sequence operations that might fail
40
- - `match` — handle each case explicitly
17
+ A toolkit for expressing uncertainty precisely. Instead of `T | null`, `try/catch`, and loading state flag soup, you get types that name every possible state and make invalid ones unrepresentable. Each type comes with a consistent set of operations — `map`, `chain`, `match`, `getOrElse` — that compose with `pipe` and `flow`.
41
18
 
42
- You can start using these right away and learn the underlying concepts as you go.
19
+ No FP jargon required. You won't find `Monad`, `Functor`, or `Applicative` in the API.
43
20
 
44
21
  ## What's included?
45
22
 
46
23
  ### pipekit/Core
47
24
 
48
- Each of these is both a TypeScript type and a module of functions for working with values of that
49
- type — constructors, transformations, and ways to extract a value back out.
50
-
51
- - **`Option<A>`** — a value that might not exist. Replaces `T | null | undefined`. Key operations:
52
- `fromNullable`, `map`, `chain`, `filter`, `match`, `getOrElse`, `recover`.
53
- - **`Result<E, A>`** — an operation that succeeds with `A` or fails with `E`. Replaces `try/catch`.
54
- Key operations: `tryCatch`, `map`, `mapError`, `chain`, `match`, `getOrElse`, `recover`.
55
- - **`Validation<E, A>`** — like `Result`, but accumulates **all** errors instead of stopping at the
56
- first. Built for form validation. Key operations: `combine`, `combineAll`, `ap`, `map`, `match`.
57
- - **`Task<A>`**a lazy async operation that doesn't run until called. Key operations: `from`,
58
- `map`, `chain`, `all`, `delay`.
59
- - **`TaskResult<E, A>`** — a lazy async operation that can fail. `Task` + `Result` combined. Key
60
- operations: `tryCatch`, `map`, `mapError`, `chain`, `match`, `recover`.
61
- - **`TaskOption<A>`** — a lazy async operation that may return nothing. `Task` + `Option` combined.
62
- Replaces `Promise<T | null>`. Key operations: `tryCatch`, `map`, `chain`, `filter`, `match`,
63
- `getOrElse`, `toTaskResult`.
64
- - **`TaskValidation<E, A>`** — a lazy async operation that accumulates errors. `Task` + `Validation`
65
- combined. Use for async form validation or parallel async checks that all need to run. Key
66
- operations: `tryCatch`, `map`, `chain`, `ap`, `match`, `recover`.
67
- - **`These<E, A>`** — an inclusive-OR: holds an error, a success value, or both simultaneously.
68
- Unlike `Result` which is one or the other, `Both` carries a warning alongside a valid result — use
69
- it when partial success with diagnostics matters. Key operations: `toErr`, `toOk`, `toBoth`,
70
- `map`, `mapErr`, `bimap`, `chain`, `match`, `swap`, `toResult`.
71
- - **`RemoteData<E, A>`** — models the four states of a data fetch: `NotAsked`, `Loading`, `Failure`,
72
- `Success`. Replaces `{ data, loading, error }` flag soup. Key operations: `notAsked`, `loading`,
73
- `failure`, `success`, `map`, `match`, `toResult`.
74
- - **`Arr`** — array operations that return `Option` instead of throwing or returning `undefined`.
75
- Operations: `head`, `last`, `findFirst`, `findLast`, `partition`, `groupBy`, `zip`, `traverse`,
76
- and more.
77
- - **`Rec`** — record/object operations. Operations: `lookup`, `map`, `filter`, `pick`, `omit`,
78
- `merge`, and more.
25
+ - **`Option<A>`** a value that might not exist. Replaces `T | null | undefined`.
26
+ - **`Result<E, A>`** an operation that succeeds or fails. Replaces `try/catch`.
27
+ - **`Validation<E, A>`** — like `Result`, but accumulates all errors instead of stopping at the first.
28
+ - **`Task<A>`** — a lazy async operation that doesn't run until called.
29
+ - **`TaskResult<E, A>`** `Task` + `Result`. A lazy async operation that can fail.
30
+ - **`TaskOption<A>`** — `Task` + `Option`. Replaces `Promise<T | null>`.
31
+ - **`TaskValidation<E, A>`** `Task` + `Validation`. For async checks that all need to run.
32
+ - **`These<E, A>`** — an inclusive OR: holds an error, a value, or both at once.
33
+ - **`RemoteData<E, A>`** the four states of a data fetch: `NotAsked`, `Loading`, `Failure`, `Success`.
34
+ - **`Arr`**array utilities that return `Option` instead of `undefined`.
35
+ - **`Rec`** record/object utilities.
79
36
 
80
37
  ### pipekit/Types
81
38
 
82
- - **`Brand<K, T>`** — nominal typing for values that share the same underlying type. Prevents
83
- passing a `CustomerId` where a `UserId` is expected, even though both are `string`. The brand
84
- exists only at compile time — zero runtime cost. Key operations: `make`, `unwrap`.
39
+ - **`Brand<K, T>`** — nominal typing at compile time, zero runtime cost.
85
40
  - **`NonEmptyList<A>`** — an array guaranteed to have at least one element.
86
41
 
87
42
  ### pipekit/Composition
88
43
 
89
- - **`pipe`** — pass a value through a series of functions, left to right.
90
- - **`flow`** compose functions into a reusable pipeline (like `pipe` without an initial value).
91
- - **`compose`** — compose functions right to left (traditional mathematical composition).
92
- - **`curry` / `uncurry`** — convert between multi-argument and single-argument functions.
93
- - **`tap`** — run a side effect (like logging) without breaking the pipeline.
94
- - **`memoize`** — cache function results.
95
- - **`identity`**, **`constant`**, **`not`**, **`and`**, **`or`**, **`once`**, **`flip`** — common
96
- function utilities.
44
+ - **`pipe`**, **`flow`**, **`compose`** — function composition.
45
+ - **`curry`** / **`uncurry`**, **`tap`**, **`memoize`**, and other function utilities.
97
46
 
98
- ## What does "composition-centric" mean?
99
-
100
- Everything in the library is designed to work with `pipe` — a function that passes a value through a
101
- series of transformations, top to bottom. The alternative is nesting calls inside each other, which
102
- reads inside-out:
47
+ ## Example
103
48
 
104
49
  ```ts
105
- import { Option } from "@nlozgachev/pipekit/Core";
50
+ import { Option, Result } from "@nlozgachev/pipekit/Core";
106
51
  import { pipe } from "@nlozgachev/pipekit/Composition";
107
52
 
108
- // Without pipe: execution order is the reverse of reading order
109
- const userName = Option.getOrElse(
110
- Option.map(
111
- Option.fromNullable(users.get("123")),
112
- (u) => u.name,
113
- ),
114
- "Anonymous",
115
- );
116
-
117
- // With pipe: reads top to bottom, matches execution order
118
- const userName = pipe(
119
- users.get("123"), // User | undefined
120
- Option.fromNullable, // Option<User>
121
- Option.map((u) => u.name), // Option<string>
122
- Option.getOrElse("Anonymous"), // string
53
+ // Chain nullable lookups without nested null checks
54
+ const city = pipe(
55
+ getUser(userId), // User | null
56
+ Option.fromNullable, // Option<User>
57
+ Option.chain((u) => Option.fromNullable(u.address)),// Option<Address>
58
+ Option.chain((a) => Option.fromNullable(a.city)), // Option<string>
59
+ Option.map((c) => c.toUpperCase()), // Option<string>
60
+ Option.getOrElse("UNKNOWN"), // string
123
61
  );
124
- ```
125
-
126
- No method chaining, no class hierarchies. Just functions that connect together.
127
-
128
- ## What does "data-last" mean and why should I care?
129
-
130
- Every operation takes the data it operates on as the **last** argument. This means you can partially
131
- apply them — get a function back without providing data yet — which makes `flow` work naturally.
132
-
133
- ```ts
134
- import { Option } from "@nlozgachev/pipekit/Core";
135
- import { flow } from "@nlozgachev/pipekit/Composition";
136
-
137
- // Data-first: can't partially apply, so you're stuck writing wrapper functions
138
- function formatName(user: User | null): string {
139
- const opt = Option.fromNullable(user);
140
- const name = Option.map(opt, (u) => u.name);
141
- return Option.getOrElse(name, "Anonymous");
142
- }
143
-
144
- users.map((u) => formatName(u)); // needs an arrow function wrapper
145
-
146
- // Data-last: operations are curried, so flow connects them directly
147
- const formatName = flow(
148
- Option.fromNullable<User>,
149
- Option.map((u) => u.name),
150
- Option.getOrElse("Anonymous"),
151
- );
152
-
153
- users.map(formatName); // no wrapper — it's already a function of User
154
- ```
155
-
156
- ## How does this help me write safer code?
157
-
158
- The core types make invalid states unrepresentable. Instead of stacking null checks and hoping you
159
- handled every branch, the shape of the code reflects the shape of the data:
160
-
161
- ```ts
162
- // Before: null handling that doesn't scale
163
- function getDisplayCity(userId: string): string {
164
- const user = getUser(userId);
165
- if (user === null) return "UNKNOWN";
166
- if (user.address === null) return "UNKNOWN";
167
- if (user.address.city === null) return "UNKNOWN";
168
- return user.address.city.toUpperCase();
169
- }
170
-
171
- // After: flat, readable, same guarantees
172
- function getDisplayCity(userId: string): string {
173
- return pipe(
174
- getUser(userId), // User | null
175
- Option.fromNullable, // Option<User>
176
- Option.chain((u) => Option.fromNullable(u.address)), // Option<Address>
177
- Option.chain((a) => Option.fromNullable(a.city)), // Option<string>
178
- Option.map((c) => c.toUpperCase()), // Option<string>
179
- Option.getOrElse("UNKNOWN"), // string
180
- );
181
- }
182
- ```
183
-
184
- `Brand` applies the same idea at the type level. When `userId` and `customerId` are both `string`,
185
- nothing stops you from passing one where the other is expected — until you brand them:
186
62
 
187
- ```ts
188
- import { Brand } from "@nlozgachev/pipekit/Types";
189
-
190
- type UserId = Brand<"UserId", string>;
191
- type CustomerId = Brand<"CustomerId", string>;
192
-
193
- const toUserId = Brand.make<"UserId", string>();
194
- const toCustomerId = Brand.make<"CustomerId", string>();
195
-
196
- function getUser(id: UserId): User {/* ... */}
197
-
198
- const cid = toCustomerId("c-99");
199
- getUser(cid); // TypeError: Argument of type 'CustomerId' is not assignable to parameter of type 'UserId'
200
- getUser(toUserId("u-42")); // ✓
201
- ```
202
-
203
- The same idea applies to error handling with `Result`, form validation with `Validation`, async
204
- operations with `Task`, `TaskResult`, `TaskOption`, and `TaskValidation`, and loading states with
205
- `RemoteData`.
206
-
207
- ## How do I install it?
208
-
209
- ```sh
210
- # Deno
211
- deno add jsr:@nlozgachev/pipekit
212
-
213
- # npm / pnpm / yarn / bun
214
- npm add @nlozgachev/pipekit
215
- ```
216
-
217
- ## How do I get started?
218
-
219
- Start with `pipe` and `Option`. These two cover the most common pain point — dealing with values
220
- that might not exist:
221
-
222
- ```ts
223
- import { Option } from "@nlozgachev/pipekit/Core";
224
- import { pipe } from "@nlozgachev/pipekit/Composition";
225
-
226
- // Read a user's preferred language from their settings, fall back to the app default
227
- const language = pipe(
228
- userSettings.get(userId), // UserSettings | undefined
229
- Option.fromNullable, // Option<UserSettings>
230
- Option.map((s) => s.language), // Option<string>
231
- Option.getOrElse(DEFAULT_LANGUAGE), // string
232
- );
233
- ```
234
-
235
- Once that feels natural, reach for `Result` when operations can fail with a meaningful error —
236
- parsing, network calls, database lookups:
237
-
238
- ```ts
239
- import { Result } from "@nlozgachev/pipekit/Core";
240
-
241
- // Parse user input and look up the record — both steps can fail
63
+ // Parse input and look up a record — both steps can fail
242
64
  const record = pipe(
243
- parseId(rawInput), // Result<ParseError, number>
244
- Result.chain((id) => db.find(id)), // Result<ParseError | NotFoundError, Record>
245
- Result.map((r) => r.name), // Result<ParseError | NotFoundError, string>
65
+ parseId(rawInput), // Result<ParseError, number>
66
+ Result.chain((id) => db.find(id)), // Result<ParseError | NotFoundError, Record>
67
+ Result.map((r) => r.name), // Result<ParseError | NotFoundError, string>
246
68
  );
247
69
  ```
248
70
 
249
- And `Validation` when you need to collect multiple errors at once, like form validation. For async
250
- equivalents of all three, reach for `TaskOption`, `TaskResult`, and `TaskValidation`.
71
+ ## Documentation
72
+
73
+ Full guides and API reference at **[pipekit.lozgachev.dev](https://pipekit.lozgachev.dev)**.
251
74
 
252
75
  ## License
253
76
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nlozgachev/pipekit",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Simple functional programming toolkit for TypeScript",
5
5
  "keywords": [
6
6
  "functional",
@@ -9,6 +9,7 @@
9
9
  "composition",
10
10
  "pipe"
11
11
  ],
12
+ "homepage": "https://pipekit.lozgachev.dev",
12
13
  "repository": {
13
14
  "type": "git",
14
15
  "url": "https://github.com/nlozgachev/pipekit"