@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.
- package/README.md +40 -217
- 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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
11
|
+
# Deno
|
|
12
|
+
deno add jsr:@nlozgachev/pipekit
|
|
13
|
+
```
|
|
32
14
|
|
|
33
|
-
|
|
34
|
-
`Applicative` in the API. Instead, you'll work with names that describe what they do:
|
|
15
|
+
## What is this?
|
|
35
16
|
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
- **`
|
|
52
|
-
|
|
53
|
-
- **`
|
|
54
|
-
|
|
55
|
-
- **`
|
|
56
|
-
|
|
57
|
-
- **`
|
|
58
|
-
|
|
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
|
|
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`** —
|
|
90
|
-
- **`
|
|
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
|
-
##
|
|
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
|
-
//
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
),
|
|
114
|
-
|
|
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
|
-
|
|
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),
|
|
244
|
-
Result.chain((id) => db.find(id)),
|
|
245
|
-
Result.map((r) => r.name),
|
|
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
|
-
|
|
250
|
-
|
|
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.
|
|
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"
|