@nlozgachev/pipekit 0.1.6 → 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 +42 -147
- 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 +2 -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,181 +2,76 @@
|
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
```sh
|
|
8
|
+
# npm / pnpm / yarn / bun
|
|
9
|
+
npm add @nlozgachev/pipekit
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# Deno
|
|
12
|
+
deno add jsr:@nlozgachev/pipekit
|
|
13
|
+
```
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
## What is this?
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
- `Result` — an operation that can succeed or fail
|
|
17
|
-
- `map` — transform a value inside a container
|
|
18
|
-
- `chain` — sequence operations that might fail
|
|
19
|
-
- `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`.
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
No FP jargon required. You won't find `Monad`, `Functor`, or `Applicative` in the API.
|
|
22
20
|
|
|
23
21
|
## What's included?
|
|
24
22
|
|
|
25
23
|
### pipekit/Core
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
- **`
|
|
30
|
-
- **`
|
|
31
|
-
- **`
|
|
32
|
-
- **`
|
|
33
|
-
- **`
|
|
34
|
-
- **`
|
|
35
|
-
- **`
|
|
36
|
-
- **`
|
|
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.
|
|
37
36
|
|
|
38
37
|
### pipekit/Types
|
|
39
38
|
|
|
39
|
+
- **`Brand<K, T>`** — nominal typing at compile time, zero runtime cost.
|
|
40
40
|
- **`NonEmptyList<A>`** — an array guaranteed to have at least one element.
|
|
41
41
|
|
|
42
42
|
### pipekit/Composition
|
|
43
43
|
|
|
44
|
-
- **`pipe`** —
|
|
45
|
-
- **`
|
|
46
|
-
- **`compose`** — compose functions right to left (traditional mathematical composition).
|
|
47
|
-
- **`curry` / `uncurry`** — convert between multi-argument and single-argument functions.
|
|
48
|
-
- **`tap`** — run a side effect (like logging) without breaking the pipeline.
|
|
49
|
-
- **`memoize`** — cache function results.
|
|
50
|
-
- **`identity`**, **`constant`**, **`not`**, **`and`**, **`or`**, **`once`**, **`flip`** — common function utilities.
|
|
44
|
+
- **`pipe`**, **`flow`**, **`compose`** — function composition.
|
|
45
|
+
- **`curry`** / **`uncurry`**, **`tap`**, **`memoize`**, and other function utilities.
|
|
51
46
|
|
|
52
|
-
##
|
|
53
|
-
|
|
54
|
-
Everything in the library is designed to work with `pipe` — a function that passes a value through a series of transformations, top to bottom. The alternative is nesting calls inside each other, which reads inside-out:
|
|
47
|
+
## Example
|
|
55
48
|
|
|
56
49
|
```ts
|
|
57
|
-
import { Option } from "@nlozgachev/pipekit/Core";
|
|
50
|
+
import { Option, Result } from "@nlozgachev/pipekit/Core";
|
|
58
51
|
import { pipe } from "@nlozgachev/pipekit/Composition";
|
|
59
52
|
|
|
60
|
-
//
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
),
|
|
66
|
-
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
// With pipe: reads top to bottom, matches execution order
|
|
70
|
-
const userName = pipe(
|
|
71
|
-
users.get("123"), // User | undefined
|
|
72
|
-
Option.fromNullable, // Option<User>
|
|
73
|
-
Option.map(u => u.name), // Option<string>
|
|
74
|
-
Option.getOrElse("Anonymous") // string
|
|
75
|
-
);
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
No method chaining, no class hierarchies. Just functions that connect together.
|
|
79
|
-
|
|
80
|
-
## What does "data-last" mean and why should I care?
|
|
81
|
-
|
|
82
|
-
Every operation takes the data it operates on as the **last** argument. This means you can partially apply them — get a function back without providing data yet — which makes `flow` work naturally.
|
|
83
|
-
|
|
84
|
-
```ts
|
|
85
|
-
import { Option } from "@nlozgachev/pipekit/Core";
|
|
86
|
-
import { flow } from "@nlozgachev/pipekit/Composition";
|
|
87
|
-
|
|
88
|
-
// Data-first: can't partially apply, so you're stuck writing wrapper functions
|
|
89
|
-
function formatName(user: User | null): string {
|
|
90
|
-
const opt = Option.fromNullable(user);
|
|
91
|
-
const name = Option.map(opt, u => u.name);
|
|
92
|
-
return Option.getOrElse(name, "Anonymous");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
users.map(u => formatName(u)); // needs an arrow function wrapper
|
|
96
|
-
|
|
97
|
-
// Data-last: operations are curried, so flow connects them directly
|
|
98
|
-
const formatName = flow(
|
|
99
|
-
Option.fromNullable<User>,
|
|
100
|
-
Option.map(u => u.name),
|
|
101
|
-
Option.getOrElse("Anonymous")
|
|
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
|
|
102
61
|
);
|
|
103
62
|
|
|
104
|
-
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
## How does this help me write safer code?
|
|
108
|
-
|
|
109
|
-
The core types make invalid states unrepresentable. Instead of stacking null checks and hoping you handled every branch, the shape of the code reflects the shape of the data:
|
|
110
|
-
|
|
111
|
-
```ts
|
|
112
|
-
// Before: null handling that doesn't scale
|
|
113
|
-
function getDisplayCity(userId: string): string {
|
|
114
|
-
const user = getUser(userId);
|
|
115
|
-
if (user === null) return "UNKNOWN";
|
|
116
|
-
if (user.address === null) return "UNKNOWN";
|
|
117
|
-
if (user.address.city === null) return "UNKNOWN";
|
|
118
|
-
return user.address.city.toUpperCase();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// After: flat, readable, same guarantees
|
|
122
|
-
function getDisplayCity(userId: string): string {
|
|
123
|
-
return pipe(
|
|
124
|
-
getUser(userId), // User | null
|
|
125
|
-
Option.fromNullable, // Option<User>
|
|
126
|
-
Option.chain(u => Option.fromNullable(u.address)), // Option<Address>
|
|
127
|
-
Option.chain(a => Option.fromNullable(a.city)), // Option<string>
|
|
128
|
-
Option.map(c => c.toUpperCase()), // Option<string>
|
|
129
|
-
Option.getOrElse("UNKNOWN") // string
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
The same idea applies to error handling with `Result`, form validation with `Validation`, async operations with `Task` and `TaskResult`, and loading states with `RemoteData`.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
## How do I install it?
|
|
139
|
-
|
|
140
|
-
```sh
|
|
141
|
-
# Deno
|
|
142
|
-
deno add jsr:@nlozgachev/pipekit
|
|
143
|
-
|
|
144
|
-
# npm / pnpm / yarn / bun
|
|
145
|
-
npm add @nlozgachev/pipekit
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## How do I get started?
|
|
149
|
-
|
|
150
|
-
Start with `pipe` and `Option`. These two cover the most common pain point — dealing with values that might not exist:
|
|
151
|
-
|
|
152
|
-
```ts
|
|
153
|
-
import { Option } from "@nlozgachev/pipekit/Core";
|
|
154
|
-
import { pipe } from "@nlozgachev/pipekit/Composition";
|
|
155
|
-
|
|
156
|
-
// Read a user's preferred language from their settings, fall back to the app default
|
|
157
|
-
const language = pipe(
|
|
158
|
-
userSettings.get(userId), // UserSettings | undefined
|
|
159
|
-
Option.fromNullable, // Option<UserSettings>
|
|
160
|
-
Option.map(s => s.language), // Option<string>
|
|
161
|
-
Option.getOrElse(DEFAULT_LANGUAGE) // string
|
|
162
|
-
);
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
Once that feels natural, reach for `Result` when operations can fail with a meaningful error — parsing, network calls, database lookups:
|
|
166
|
-
|
|
167
|
-
```ts
|
|
168
|
-
import { Result } from "@nlozgachev/pipekit/Core";
|
|
169
|
-
|
|
170
|
-
// Parse user input and look up the record — both steps can fail
|
|
63
|
+
// Parse input and look up a record — both steps can fail
|
|
171
64
|
const record = pipe(
|
|
172
|
-
parseId(rawInput),
|
|
173
|
-
Result.chain(id => db.find(id)),
|
|
174
|
-
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>
|
|
175
68
|
);
|
|
176
69
|
```
|
|
177
70
|
|
|
178
|
-
|
|
71
|
+
## Documentation
|
|
72
|
+
|
|
73
|
+
Full guides and API reference at **[pipekit.lozgachev.dev](https://pipekit.lozgachev.dev)**.
|
|
179
74
|
|
|
180
75
|
## License
|
|
181
76
|
|
|
182
|
-
MIT
|
|
77
|
+
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 = {}));
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { Result } from "./Result.js";
|
|
2
|
+
export var These;
|
|
3
|
+
(function (These) {
|
|
4
|
+
/**
|
|
5
|
+
* Creates a These holding only an error/warning (no success value).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* These.toErr("Something went wrong");
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
These.toErr = (error) => Result.toErr(error);
|
|
13
|
+
/**
|
|
14
|
+
* Creates a These holding only a success value (no error).
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* These.toOk(42);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
These.toOk = (value) => Result.toOk(value);
|
|
22
|
+
/**
|
|
23
|
+
* Creates a These holding both an error/warning and a success value.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* These.toBoth("Deprecated API used", result);
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
These.toBoth = (error, value) => ({
|
|
31
|
+
kind: "Both",
|
|
32
|
+
error,
|
|
33
|
+
value,
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* Type guard — checks if a These holds only an error/warning.
|
|
37
|
+
*/
|
|
38
|
+
These.isErr = (data) => data.kind === "Error";
|
|
39
|
+
/**
|
|
40
|
+
* Type guard — checks if a These holds only a success value.
|
|
41
|
+
*/
|
|
42
|
+
These.isOk = (data) => data.kind === "Ok";
|
|
43
|
+
/**
|
|
44
|
+
* Type guard — checks if a These holds both an error/warning and a success value.
|
|
45
|
+
*/
|
|
46
|
+
These.isBoth = (data) => data.kind === "Both";
|
|
47
|
+
/**
|
|
48
|
+
* Returns true if the These contains a success value (Ok or Both).
|
|
49
|
+
*/
|
|
50
|
+
These.hasValue = (data) => data.kind === "Ok" || data.kind === "Both";
|
|
51
|
+
/**
|
|
52
|
+
* Returns true if the These contains an error/warning (Err or Both).
|
|
53
|
+
*/
|
|
54
|
+
These.hasError = (data) => data.kind === "Error" || data.kind === "Both";
|
|
55
|
+
/**
|
|
56
|
+
* Transforms the success value, leaving the error unchanged.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* pipe(These.toOk(5), These.map(n => n * 2)); // Ok(10)
|
|
61
|
+
* pipe(These.toBoth("warn", 5), These.map(n => n * 2)); // Both("warn", 10)
|
|
62
|
+
* pipe(These.toErr("err"), These.map(n => n * 2)); // Err("err")
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
These.map = (f) => (data) => {
|
|
66
|
+
if (These.isErr(data))
|
|
67
|
+
return data;
|
|
68
|
+
if (These.isOk(data))
|
|
69
|
+
return These.toOk(f(data.value));
|
|
70
|
+
return These.toBoth(data.error, f(data.value));
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Transforms the error/warning value, leaving the success value unchanged.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* pipe(These.toErr("err"), These.mapErr(e => e.toUpperCase())); // Err("ERR")
|
|
78
|
+
* pipe(These.toBoth("warn", 5), These.mapErr(e => e.toUpperCase())); // Both("WARN", 5)
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
These.mapErr = (f) => (data) => {
|
|
82
|
+
if (These.isOk(data))
|
|
83
|
+
return data;
|
|
84
|
+
if (These.isErr(data))
|
|
85
|
+
return These.toErr(f(data.error));
|
|
86
|
+
return These.toBoth(f(data.error), data.value);
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Transforms both the error and success values independently.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* pipe(
|
|
94
|
+
* These.toBoth("warn", 5),
|
|
95
|
+
* These.bimap(e => e.toUpperCase(), n => n * 2)
|
|
96
|
+
* ); // Both("WARN", 10)
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
These.bimap = (onErr, onOk) => (data) => {
|
|
100
|
+
if (These.isErr(data))
|
|
101
|
+
return These.toErr(onErr(data.error));
|
|
102
|
+
if (These.isOk(data))
|
|
103
|
+
return These.toOk(onOk(data.value));
|
|
104
|
+
return These.toBoth(onErr(data.error), onOk(data.value));
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Chains These computations by passing the success value to f.
|
|
108
|
+
* - Err propagates unchanged.
|
|
109
|
+
* - Ok(a) applies f(a) directly.
|
|
110
|
+
* - Both(e, a): applies f(a); if the result is Ok(b), returns Both(e, b)
|
|
111
|
+
* to preserve the warning. Otherwise returns f(a) as-is.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* const double = (n: number): These<string, number> => These.toOk(n * 2);
|
|
116
|
+
*
|
|
117
|
+
* pipe(These.toOk(5), These.chain(double)); // Ok(10)
|
|
118
|
+
* pipe(These.toBoth("warn", 5), These.chain(double)); // Both("warn", 10)
|
|
119
|
+
* pipe(These.toErr("err"), These.chain(double)); // Err("err")
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
These.chain = (f) => (data) => {
|
|
123
|
+
if (These.isErr(data))
|
|
124
|
+
return data;
|
|
125
|
+
if (These.isOk(data))
|
|
126
|
+
return f(data.value);
|
|
127
|
+
const result = f(data.value);
|
|
128
|
+
return These.isOk(result) ? These.toBoth(data.error, result.value) : result;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Extracts a value from a These by providing handlers for all three cases.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* pipe(
|
|
136
|
+
* these,
|
|
137
|
+
* These.fold(
|
|
138
|
+
* e => `Error: ${e}`,
|
|
139
|
+
* a => `Value: ${a}`,
|
|
140
|
+
* (e, a) => `Both: ${e} / ${a}`
|
|
141
|
+
* )
|
|
142
|
+
* );
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
These.fold = (onErr, onOk, onBoth) => (data) => {
|
|
146
|
+
if (These.isErr(data))
|
|
147
|
+
return onErr(data.error);
|
|
148
|
+
if (These.isOk(data))
|
|
149
|
+
return onOk(data.value);
|
|
150
|
+
return onBoth(data.error, data.value);
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Pattern matches on a These, returning the result of the matching case.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* pipe(
|
|
158
|
+
* these,
|
|
159
|
+
* These.match({
|
|
160
|
+
* err: e => `Error: ${e}`,
|
|
161
|
+
* ok: a => `Value: ${a}`,
|
|
162
|
+
* both: (e, a) => `Both: ${e} / ${a}`
|
|
163
|
+
* })
|
|
164
|
+
* );
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
These.match = (cases) => (data) => {
|
|
168
|
+
if (These.isErr(data))
|
|
169
|
+
return cases.err(data.error);
|
|
170
|
+
if (These.isOk(data))
|
|
171
|
+
return cases.ok(data.value);
|
|
172
|
+
return cases.both(data.error, data.value);
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Returns the success value, or a default if the These has no success value.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* pipe(These.toOk(5), These.getOrElse(0)); // 5
|
|
180
|
+
* pipe(These.toBoth("warn", 5), These.getOrElse(0)); // 5
|
|
181
|
+
* pipe(These.toErr("err"), These.getOrElse(0)); // 0
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
These.getOrElse = (defaultValue) => (data) => These.hasValue(data) ? data.value : defaultValue;
|
|
185
|
+
/**
|
|
186
|
+
* Executes a side effect on the success value without changing the These.
|
|
187
|
+
* Useful for logging or debugging.
|
|
188
|
+
*/
|
|
189
|
+
These.tap = (f) => (data) => {
|
|
190
|
+
if (These.hasValue(data))
|
|
191
|
+
f(data.value);
|
|
192
|
+
return data;
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Swaps the roles of error and success values.
|
|
196
|
+
* - Err(e) → Ok(e)
|
|
197
|
+
* - Ok(a) → Err(a)
|
|
198
|
+
* - Both(e, a) → Both(a, e)
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```ts
|
|
202
|
+
* These.swap(These.toErr("err")); // Ok("err")
|
|
203
|
+
* These.swap(These.toOk(5)); // Err(5)
|
|
204
|
+
* These.swap(These.toBoth("warn", 5)); // Both(5, "warn")
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
These.swap = (data) => {
|
|
208
|
+
if (These.isErr(data))
|
|
209
|
+
return These.toOk(data.error);
|
|
210
|
+
if (These.isOk(data))
|
|
211
|
+
return These.toErr(data.value);
|
|
212
|
+
return These.toBoth(data.value, data.error);
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Converts a These to an Option.
|
|
216
|
+
* Ok and Both produce Some; Err produces None.
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```ts
|
|
220
|
+
* These.toOption(These.toOk(42)); // Some(42)
|
|
221
|
+
* These.toOption(These.toBoth("warn", 42)); // Some(42)
|
|
222
|
+
* These.toOption(These.toErr("err")); // None
|
|
223
|
+
* ```
|
|
224
|
+
*/
|
|
225
|
+
These.toOption = (data) => These.hasValue(data) ? { kind: "Some", value: data.value } : { kind: "None" };
|
|
226
|
+
/**
|
|
227
|
+
* Converts a These to a Result, discarding any warning from Both.
|
|
228
|
+
* Ok and Both produce Ok; Err produces Err.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```ts
|
|
232
|
+
* These.toResult(These.toOk(42)); // Ok(42)
|
|
233
|
+
* These.toResult(These.toBoth("warn", 42)); // Ok(42)
|
|
234
|
+
* These.toResult(These.toErr("err")); // Err("err")
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
These.toResult = (data) => {
|
|
238
|
+
if (These.hasValue(data))
|
|
239
|
+
return Result.toOk(data.value);
|
|
240
|
+
return data;
|
|
241
|
+
};
|
|
242
|
+
})(These || (These = {}));
|
package/esm/src/Core/index.js
CHANGED
|
@@ -4,5 +4,8 @@ export * from "./Rec.js";
|
|
|
4
4
|
export * from "./RemoteData.js";
|
|
5
5
|
export * from "./Result.js";
|
|
6
6
|
export * from "./Task.js";
|
|
7
|
+
export * from "./TaskOption.js";
|
|
7
8
|
export * from "./TaskResult.js";
|
|
9
|
+
export * from "./TaskValidation.js";
|
|
10
|
+
export * from "./These.js";
|
|
8
11
|
export * from "./Validation.js";
|