@nlozgachev/pipekit 0.1.6 → 0.1.7
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 +117 -45
- package/esm/src/Core/TaskOption.js +99 -0
- package/esm/src/Core/TaskValidation.js +93 -0
- package/esm/src/Core/These.js +242 -0
- package/esm/src/Core/index.js +3 -0
- package/esm/src/Types/Brand.js +28 -0
- package/esm/src/Types/index.js +1 -0
- package/package.json +1 -1
- package/script/src/Core/TaskOption.js +102 -0
- package/script/src/Core/TaskValidation.js +96 -0
- package/script/src/Core/These.js +245 -0
- package/script/src/Core/index.js +3 -0
- package/script/src/Types/Brand.js +31 -0
- package/script/src/Types/index.js +1 -0
- package/types/src/Core/TaskOption.d.ts +120 -0
- package/types/src/Core/TaskOption.d.ts.map +1 -0
- package/types/src/Core/TaskValidation.d.ts +115 -0
- package/types/src/Core/TaskValidation.d.ts.map +1 -0
- package/types/src/Core/These.d.ts +213 -0
- package/types/src/Core/These.d.ts.map +1 -0
- package/types/src/Core/index.d.ts +3 -0
- package/types/src/Core/index.d.ts.map +1 -1
- package/types/src/Types/Brand.d.ts +52 -0
- package/types/src/Types/Brand.d.ts.map +1 -0
- package/types/src/Types/index.d.ts +1 -0
- package/types/src/Types/index.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -2,15 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@nlozgachev/pipekit)[](https://jsr.io/@nlozgachev/pipekit)[](https://www.typescriptlang.org)[](https://deno.com)
|
|
4
4
|
|
|
5
|
-
A
|
|
5
|
+
A TypeScript toolkit for writing code that means exactly what it says.
|
|
6
6
|
|
|
7
7
|
## What is this?
|
|
8
8
|
|
|
9
|
-
A
|
|
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.
|
|
10
30
|
|
|
11
31
|
## Do I need to know functional programming to use this?
|
|
12
32
|
|
|
13
|
-
No. The library avoids FP-specific jargon wherever possible. You won't find `Monad`, `Functor`, or
|
|
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:
|
|
14
35
|
|
|
15
36
|
- `Option` — a value that might not exist
|
|
16
37
|
- `Result` — an operation that can succeed or fail
|
|
@@ -24,19 +45,43 @@ You can start using these right away and learn the underlying concepts as you go
|
|
|
24
45
|
|
|
25
46
|
### pipekit/Core
|
|
26
47
|
|
|
27
|
-
Each of these is both a TypeScript type and a module of functions for working with values of that
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- **`
|
|
31
|
-
|
|
32
|
-
- **`
|
|
33
|
-
|
|
34
|
-
- **`
|
|
35
|
-
|
|
36
|
-
- **`
|
|
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.
|
|
37
79
|
|
|
38
80
|
### pipekit/Types
|
|
39
81
|
|
|
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`.
|
|
40
85
|
- **`NonEmptyList<A>`** — an array guaranteed to have at least one element.
|
|
41
86
|
|
|
42
87
|
### pipekit/Composition
|
|
@@ -47,11 +92,14 @@ Each of these is both a TypeScript type and a module of functions for working wi
|
|
|
47
92
|
- **`curry` / `uncurry`** — convert between multi-argument and single-argument functions.
|
|
48
93
|
- **`tap`** — run a side effect (like logging) without breaking the pipeline.
|
|
49
94
|
- **`memoize`** — cache function results.
|
|
50
|
-
- **`identity`**, **`constant`**, **`not`**, **`and`**, **`or`**, **`once`**, **`flip`** — common
|
|
95
|
+
- **`identity`**, **`constant`**, **`not`**, **`and`**, **`or`**, **`once`**, **`flip`** — common
|
|
96
|
+
function utilities.
|
|
51
97
|
|
|
52
98
|
## What does "composition-centric" mean?
|
|
53
99
|
|
|
54
|
-
Everything in the library is designed to work with `pipe` — a function that passes a value through a
|
|
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:
|
|
55
103
|
|
|
56
104
|
```ts
|
|
57
105
|
import { Option } from "@nlozgachev/pipekit/Core";
|
|
@@ -61,17 +109,17 @@ import { pipe } from "@nlozgachev/pipekit/Composition";
|
|
|
61
109
|
const userName = Option.getOrElse(
|
|
62
110
|
Option.map(
|
|
63
111
|
Option.fromNullable(users.get("123")),
|
|
64
|
-
u => u.name
|
|
112
|
+
(u) => u.name,
|
|
65
113
|
),
|
|
66
|
-
"Anonymous"
|
|
114
|
+
"Anonymous",
|
|
67
115
|
);
|
|
68
116
|
|
|
69
117
|
// With pipe: reads top to bottom, matches execution order
|
|
70
118
|
const userName = pipe(
|
|
71
|
-
users.get("123"),
|
|
72
|
-
Option.fromNullable,
|
|
73
|
-
Option.map(u => u.name),
|
|
74
|
-
Option.getOrElse("Anonymous")
|
|
119
|
+
users.get("123"), // User | undefined
|
|
120
|
+
Option.fromNullable, // Option<User>
|
|
121
|
+
Option.map((u) => u.name), // Option<string>
|
|
122
|
+
Option.getOrElse("Anonymous"), // string
|
|
75
123
|
);
|
|
76
124
|
```
|
|
77
125
|
|
|
@@ -79,7 +127,8 @@ No method chaining, no class hierarchies. Just functions that connect together.
|
|
|
79
127
|
|
|
80
128
|
## What does "data-last" mean and why should I care?
|
|
81
129
|
|
|
82
|
-
Every operation takes the data it operates on as the **last** argument. This means you can partially
|
|
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.
|
|
83
132
|
|
|
84
133
|
```ts
|
|
85
134
|
import { Option } from "@nlozgachev/pipekit/Core";
|
|
@@ -88,17 +137,17 @@ import { flow } from "@nlozgachev/pipekit/Composition";
|
|
|
88
137
|
// Data-first: can't partially apply, so you're stuck writing wrapper functions
|
|
89
138
|
function formatName(user: User | null): string {
|
|
90
139
|
const opt = Option.fromNullable(user);
|
|
91
|
-
const name = Option.map(opt, u => u.name);
|
|
140
|
+
const name = Option.map(opt, (u) => u.name);
|
|
92
141
|
return Option.getOrElse(name, "Anonymous");
|
|
93
142
|
}
|
|
94
143
|
|
|
95
|
-
users.map(u => formatName(u)); // needs an arrow function wrapper
|
|
144
|
+
users.map((u) => formatName(u)); // needs an arrow function wrapper
|
|
96
145
|
|
|
97
146
|
// Data-last: operations are curried, so flow connects them directly
|
|
98
147
|
const formatName = flow(
|
|
99
148
|
Option.fromNullable<User>,
|
|
100
|
-
Option.map(u => u.name),
|
|
101
|
-
Option.getOrElse("Anonymous")
|
|
149
|
+
Option.map((u) => u.name),
|
|
150
|
+
Option.getOrElse("Anonymous"),
|
|
102
151
|
);
|
|
103
152
|
|
|
104
153
|
users.map(formatName); // no wrapper — it's already a function of User
|
|
@@ -106,7 +155,8 @@ users.map(formatName); // no wrapper — it's already a function of User
|
|
|
106
155
|
|
|
107
156
|
## How does this help me write safer code?
|
|
108
157
|
|
|
109
|
-
The core types make invalid states unrepresentable. Instead of stacking null checks and hoping you
|
|
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:
|
|
110
160
|
|
|
111
161
|
```ts
|
|
112
162
|
// Before: null handling that doesn't scale
|
|
@@ -121,19 +171,38 @@ function getDisplayCity(userId: string): string {
|
|
|
121
171
|
// After: flat, readable, same guarantees
|
|
122
172
|
function getDisplayCity(userId: string): string {
|
|
123
173
|
return pipe(
|
|
124
|
-
getUser(userId),
|
|
125
|
-
Option.fromNullable,
|
|
126
|
-
Option.chain(u => Option.fromNullable(u.address)),
|
|
127
|
-
Option.chain(a => Option.fromNullable(a.city)),
|
|
128
|
-
Option.map(c => c.toUpperCase()),
|
|
129
|
-
Option.getOrElse("UNKNOWN")
|
|
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
|
|
130
180
|
);
|
|
131
181
|
}
|
|
132
182
|
```
|
|
133
183
|
|
|
134
|
-
|
|
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
|
+
|
|
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>();
|
|
135
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
|
+
```
|
|
136
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`.
|
|
137
206
|
|
|
138
207
|
## How do I install it?
|
|
139
208
|
|
|
@@ -147,7 +216,8 @@ npm add @nlozgachev/pipekit
|
|
|
147
216
|
|
|
148
217
|
## How do I get started?
|
|
149
218
|
|
|
150
|
-
Start with `pipe` and `Option`. These two cover the most common pain point — dealing with values
|
|
219
|
+
Start with `pipe` and `Option`. These two cover the most common pain point — dealing with values
|
|
220
|
+
that might not exist:
|
|
151
221
|
|
|
152
222
|
```ts
|
|
153
223
|
import { Option } from "@nlozgachev/pipekit/Core";
|
|
@@ -155,28 +225,30 @@ import { pipe } from "@nlozgachev/pipekit/Composition";
|
|
|
155
225
|
|
|
156
226
|
// Read a user's preferred language from their settings, fall back to the app default
|
|
157
227
|
const language = pipe(
|
|
158
|
-
userSettings.get(userId),
|
|
159
|
-
Option.fromNullable,
|
|
160
|
-
Option.map(s => s.language),
|
|
161
|
-
Option.getOrElse(DEFAULT_LANGUAGE) // string
|
|
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
|
|
162
232
|
);
|
|
163
233
|
```
|
|
164
234
|
|
|
165
|
-
Once that feels natural, reach for `Result` when operations can fail with a meaningful error —
|
|
235
|
+
Once that feels natural, reach for `Result` when operations can fail with a meaningful error —
|
|
236
|
+
parsing, network calls, database lookups:
|
|
166
237
|
|
|
167
238
|
```ts
|
|
168
239
|
import { Result } from "@nlozgachev/pipekit/Core";
|
|
169
240
|
|
|
170
241
|
// Parse user input and look up the record — both steps can fail
|
|
171
242
|
const record = pipe(
|
|
172
|
-
parseId(rawInput),
|
|
173
|
-
Result.chain(id => db.find(id)),
|
|
174
|
-
Result.map(r => r.name),
|
|
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>
|
|
175
246
|
);
|
|
176
247
|
```
|
|
177
248
|
|
|
178
|
-
And `Validation` when you need to collect multiple errors at once, like form validation.
|
|
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`.
|
|
179
251
|
|
|
180
252
|
## License
|
|
181
253
|
|
|
182
|
-
MIT
|
|
254
|
+
MIT
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Option } from "./Option.js";
|
|
2
|
+
import { Task } from "./Task.js";
|
|
3
|
+
export var TaskOption;
|
|
4
|
+
(function (TaskOption) {
|
|
5
|
+
/**
|
|
6
|
+
* Wraps a value in a Some inside a Task.
|
|
7
|
+
*/
|
|
8
|
+
TaskOption.of = (value) => Task.of(Option.of(value));
|
|
9
|
+
/**
|
|
10
|
+
* Creates a TaskOption that resolves to None.
|
|
11
|
+
*/
|
|
12
|
+
TaskOption.none = () => Task.of(Option.toNone());
|
|
13
|
+
/**
|
|
14
|
+
* Lifts an Option into a TaskOption.
|
|
15
|
+
*/
|
|
16
|
+
TaskOption.fromOption = (option) => Task.of(option);
|
|
17
|
+
/**
|
|
18
|
+
* Lifts a Task into a TaskOption by wrapping its result in Some.
|
|
19
|
+
*/
|
|
20
|
+
TaskOption.fromTask = (task) => Task.map(Option.of)(task);
|
|
21
|
+
/**
|
|
22
|
+
* Creates a TaskOption from a Promise-returning function.
|
|
23
|
+
* Returns Some if the promise resolves, None if it rejects.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const fetchUser = TaskOption.tryCatch(() =>
|
|
28
|
+
* fetch("/user/1").then(r => r.json())
|
|
29
|
+
* );
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
TaskOption.tryCatch = (f) => () => f().then(Option.of).catch(() => Option.toNone());
|
|
33
|
+
/**
|
|
34
|
+
* Transforms the value inside a TaskOption.
|
|
35
|
+
*/
|
|
36
|
+
TaskOption.map = (f) => (data) => Task.map(Option.map(f))(data);
|
|
37
|
+
/**
|
|
38
|
+
* Chains TaskOption computations. If the first resolves to Some, passes the
|
|
39
|
+
* value to f. If the first resolves to None, propagates None.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* pipe(
|
|
44
|
+
* findUser("123"),
|
|
45
|
+
* TaskOption.chain(user => findOrg(user.orgId))
|
|
46
|
+
* )();
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
TaskOption.chain = (f) => (data) => Task.chain((option) => Option.isSome(option) ? f(option.value) : Task.of(Option.toNone()))(data);
|
|
50
|
+
/**
|
|
51
|
+
* Applies a function wrapped in a TaskOption to a value wrapped in a TaskOption.
|
|
52
|
+
* Both Tasks run in parallel.
|
|
53
|
+
*/
|
|
54
|
+
TaskOption.ap = (arg) => (data) => () => Promise.all([data(), arg()]).then(([of_, oa]) => Option.ap(oa)(of_));
|
|
55
|
+
/**
|
|
56
|
+
* Extracts a value from a TaskOption by providing handlers for both cases.
|
|
57
|
+
*/
|
|
58
|
+
TaskOption.fold = (onNone, onSome) => (data) => Task.map(Option.fold(onNone, onSome))(data);
|
|
59
|
+
/**
|
|
60
|
+
* Pattern matches on a TaskOption, returning a Task of the result.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* pipe(
|
|
65
|
+
* findUser("123"),
|
|
66
|
+
* TaskOption.match({
|
|
67
|
+
* some: user => `Hello, ${user.name}`,
|
|
68
|
+
* none: () => "User not found"
|
|
69
|
+
* })
|
|
70
|
+
* )();
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
TaskOption.match = (cases) => (data) => Task.map(Option.match(cases))(data);
|
|
74
|
+
/**
|
|
75
|
+
* Returns the value or a default if the TaskOption resolves to None.
|
|
76
|
+
*/
|
|
77
|
+
TaskOption.getOrElse = (defaultValue) => (data) => Task.map(Option.getOrElse(defaultValue))(data);
|
|
78
|
+
/**
|
|
79
|
+
* Executes a side effect on the value without changing the TaskOption.
|
|
80
|
+
* Useful for logging or debugging.
|
|
81
|
+
*/
|
|
82
|
+
TaskOption.tap = (f) => (data) => Task.map(Option.tap(f))(data);
|
|
83
|
+
/**
|
|
84
|
+
* Filters the value inside a TaskOption. Returns None if the predicate fails.
|
|
85
|
+
*/
|
|
86
|
+
TaskOption.filter = (predicate) => (data) => Task.map(Option.filter(predicate))(data);
|
|
87
|
+
/**
|
|
88
|
+
* Converts a TaskOption to a TaskResult, using onNone to produce the error value.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* pipe(
|
|
93
|
+
* findUser("123"),
|
|
94
|
+
* TaskOption.toTaskResult(() => "User not found")
|
|
95
|
+
* );
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
TaskOption.toTaskResult = (onNone) => (data) => Task.map(Option.toResult(onNone))(data);
|
|
99
|
+
})(TaskOption || (TaskOption = {}));
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Task } from "./Task.js";
|
|
2
|
+
import { Validation } from "./Validation.js";
|
|
3
|
+
export var TaskValidation;
|
|
4
|
+
(function (TaskValidation) {
|
|
5
|
+
/**
|
|
6
|
+
* Wraps a value in a valid TaskValidation.
|
|
7
|
+
*/
|
|
8
|
+
TaskValidation.of = (value) => Task.of(Validation.of(value));
|
|
9
|
+
/**
|
|
10
|
+
* Creates a failed TaskValidation with a single error.
|
|
11
|
+
*/
|
|
12
|
+
TaskValidation.fail = (error) => Task.of(Validation.fail(error));
|
|
13
|
+
/**
|
|
14
|
+
* Lifts a Validation into a TaskValidation.
|
|
15
|
+
*/
|
|
16
|
+
TaskValidation.fromValidation = (validation) => Task.of(validation);
|
|
17
|
+
/**
|
|
18
|
+
* Creates a TaskValidation from a Promise-returning function.
|
|
19
|
+
* Catches any errors and transforms them using the onError function.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const fetchUser = (id: string): TaskValidation<string, User> =>
|
|
24
|
+
* TaskValidation.tryCatch(
|
|
25
|
+
* () => fetch(`/users/${id}`).then(r => r.json()),
|
|
26
|
+
* e => `Failed to fetch user: ${e}`
|
|
27
|
+
* );
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
TaskValidation.tryCatch = (f, onError) => () => f()
|
|
31
|
+
.then((Validation.of))
|
|
32
|
+
.catch((e) => Validation.fail(onError(e)));
|
|
33
|
+
/**
|
|
34
|
+
* Transforms the success value inside a TaskValidation.
|
|
35
|
+
*/
|
|
36
|
+
TaskValidation.map = (f) => (data) => Task.map(Validation.map(f))(data);
|
|
37
|
+
/**
|
|
38
|
+
* Chains TaskValidation computations. If the first is Valid, passes the value
|
|
39
|
+
* to f. If the first is Invalid, propagates the errors.
|
|
40
|
+
*
|
|
41
|
+
* Note: chain short-circuits on first error. Use ap to accumulate errors.
|
|
42
|
+
*/
|
|
43
|
+
TaskValidation.chain = (f) => (data) => Task.chain((validation) => Validation.isValid(validation)
|
|
44
|
+
? f(validation.value)
|
|
45
|
+
: Task.of(Validation.toInvalid(validation.errors)))(data);
|
|
46
|
+
/**
|
|
47
|
+
* Applies a function wrapped in a TaskValidation to a value wrapped in a
|
|
48
|
+
* TaskValidation. Both Tasks run in parallel and errors from both sides
|
|
49
|
+
* are accumulated.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* pipe(
|
|
54
|
+
* TaskValidation.of((name: string) => (age: number) => ({ name, age })),
|
|
55
|
+
* TaskValidation.ap(validateName(name)),
|
|
56
|
+
* TaskValidation.ap(validateAge(age))
|
|
57
|
+
* )();
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
TaskValidation.ap = (arg) => (data) => () => Promise.all([data(), arg()]).then(([vf, va]) => Validation.ap(va)(vf));
|
|
61
|
+
/**
|
|
62
|
+
* Extracts a value from a TaskValidation by providing handlers for both cases.
|
|
63
|
+
*/
|
|
64
|
+
TaskValidation.fold = (onInvalid, onValid) => (data) => Task.map(Validation.fold(onInvalid, onValid))(data);
|
|
65
|
+
/**
|
|
66
|
+
* Pattern matches on a TaskValidation, returning a Task of the result.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* pipe(
|
|
71
|
+
* validateForm(input),
|
|
72
|
+
* TaskValidation.match({
|
|
73
|
+
* valid: data => save(data),
|
|
74
|
+
* invalid: errors => showErrors(errors)
|
|
75
|
+
* })
|
|
76
|
+
* )();
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
TaskValidation.match = (cases) => (data) => Task.map(Validation.match(cases))(data);
|
|
80
|
+
/**
|
|
81
|
+
* Returns the success value or a default value if the TaskValidation is invalid.
|
|
82
|
+
*/
|
|
83
|
+
TaskValidation.getOrElse = (defaultValue) => (data) => Task.map(Validation.getOrElse(defaultValue))(data);
|
|
84
|
+
/**
|
|
85
|
+
* Executes a side effect on the success value without changing the TaskValidation.
|
|
86
|
+
* Useful for logging or debugging.
|
|
87
|
+
*/
|
|
88
|
+
TaskValidation.tap = (f) => (data) => Task.map(Validation.tap(f))(data);
|
|
89
|
+
/**
|
|
90
|
+
* Recovers from an Invalid state by providing a fallback TaskValidation.
|
|
91
|
+
*/
|
|
92
|
+
TaskValidation.recover = (fallback) => (data) => Task.chain((validation) => Validation.isValid(validation) ? Task.of(validation) : fallback())(data);
|
|
93
|
+
})(TaskValidation || (TaskValidation = {}));
|