yield-result 0.1.2 → 0.1.4
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 +124 -3
- package/dist/types.d.ts +16 -1
- package/dist/types.js +15 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> Rust-style `?` operator for TypeScript using generators (`yield*`). Zero dependencies, lightweight, type-safe error handling without runtime exceptions.
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/yield-result)
|
|
6
|
+
[](https://bundlephobia.com/package/yield-result)
|
|
5
7
|
[](https://www.typescriptlang.org/)
|
|
6
8
|
[](https://vitest.dev/)
|
|
7
9
|
[](LICENSE)
|
|
@@ -129,10 +131,129 @@ const fetchRes = await fromPromise(
|
|
|
129
131
|
const asyncRes = await fromAsyncFn(() => loadData());
|
|
130
132
|
```
|
|
131
133
|
|
|
134
|
+
### 4. Strongly-Typed Errors with `taggedError`
|
|
135
|
+
|
|
136
|
+
Use `taggedError` to create clean, discriminated error unions with custom payloads:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import { taggedError, err, safe, match } from "yield-result";
|
|
140
|
+
|
|
141
|
+
// Define domain errors
|
|
142
|
+
const HttpError = (status: number) => taggedError("HttpError", { status });
|
|
143
|
+
const ValidationError = (field: string) => taggedError("ValidationError", { field });
|
|
144
|
+
|
|
145
|
+
// Functions returning different error types
|
|
146
|
+
const fetchUser = (id: string) => err(HttpError(404));
|
|
147
|
+
const validateAge = (age: number) => err(ValidationError("age"));
|
|
148
|
+
|
|
149
|
+
// safe.sync automatically infers the error union: Result<User, TaggedError<"HttpError"> | TaggedError<"ValidationError">>
|
|
150
|
+
const result = safe.sync(function* () {
|
|
151
|
+
const user = yield* fetchUser("123");
|
|
152
|
+
yield* validateAge(user.age);
|
|
153
|
+
return user;
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 🏷️ Automatic Error Union Inference (`InferYieldErr`)
|
|
160
|
+
|
|
161
|
+
`yield-result` features automatic error type extraction and union distribution (`InferYieldErr<G>`). When yielding different error types across pipeline steps, TypeScript automatically unifies them without requiring manual type annotations!
|
|
162
|
+
|
|
163
|
+
### Before vs After
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
type DbError = { _tag: "DbError"; code: string };
|
|
167
|
+
type ValidationError = { _tag: "ValidationError"; field: string };
|
|
168
|
+
|
|
169
|
+
const queryUser = (): Result<User, DbError> => ...;
|
|
170
|
+
const validateUser = (u: User): Result<boolean, ValidationError> => ...;
|
|
171
|
+
|
|
172
|
+
// ❌ BEFORE: Manual generic type parameters required:
|
|
173
|
+
const res = safe.sync<User, DbError | ValidationError>(function* () {
|
|
174
|
+
const user = yield* queryUser();
|
|
175
|
+
const valid = yield* validateUser(user);
|
|
176
|
+
return user;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ✅ AFTER: 100% AUTOMATIC INFERENCE (Zero type arguments needed!):
|
|
180
|
+
// Automatically inferred as: Result<User, DbError | ValidationError>
|
|
181
|
+
const res = safe.sync(function* () {
|
|
182
|
+
const user = yield* queryUser(); // Yields Err<DbError>
|
|
183
|
+
const valid = yield* validateUser(user); // Yields Err<ValidationError>
|
|
184
|
+
return user;
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 🌐 Complete Real-World API Workflow Example
|
|
191
|
+
|
|
192
|
+
Here is a full end-to-end production API endpoint handler (**Fetch → Validation → DB → HTTP Response**):
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { safe, ok, err, fromPromise, taggedError, match, Result } from "yield-result";
|
|
196
|
+
|
|
197
|
+
// 1. Define Discriminated Domain Errors
|
|
198
|
+
const ApiError = (status: number, message: string) => taggedError("ApiError", { status, message });
|
|
199
|
+
const DbError = (query: string) => taggedError("DbError", { query });
|
|
200
|
+
const ValidationError = (field: string, reason: string) => taggedError("ValidationError", { field, reason });
|
|
201
|
+
|
|
202
|
+
type DomainError =
|
|
203
|
+
| ReturnType<typeof ApiError>
|
|
204
|
+
| ReturnType<typeof DbError>
|
|
205
|
+
| ReturnType<typeof ValidationError>;
|
|
206
|
+
|
|
207
|
+
// 2. Domain Services
|
|
208
|
+
async function fetchExternalOrder(orderId: string): Promise<Result<{ id: string; amount: number }, DomainError>> {
|
|
209
|
+
return fromPromise(
|
|
210
|
+
fetch(`https://api.payments.com/orders/${orderId}`).then(r => r.json()),
|
|
211
|
+
() => ApiError(502, "External Payment Gateway unreachable")
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function validateOrderAmount(order: { id: string; amount: number }): Result<{ id: string; amount: number }, DomainError> {
|
|
216
|
+
if (order.amount <= 0) return err(ValidationError("amount", "Order amount must be positive"));
|
|
217
|
+
return ok(order);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function saveOrderToDb(order: { id: string; amount: number }): Promise<Result<{ saved: boolean }, DomainError>> {
|
|
221
|
+
// Simulating DB write
|
|
222
|
+
return ok({ saved: true });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 3. Complete Business Pipeline with safe.async
|
|
226
|
+
export async function handleOrderCheckout(orderId: string) {
|
|
227
|
+
const pipelineResult = await safe.async(async function* () {
|
|
228
|
+
const rawOrder = yield* await fetchExternalOrder(orderId); // Step 1: External API
|
|
229
|
+
const validOrder = yield* validateOrderAmount(rawOrder); // Step 2: Domain Validation
|
|
230
|
+
const dbRecord = yield* await saveOrderToDb(validOrder); // Step 3: Database Save
|
|
231
|
+
|
|
232
|
+
return { status: 200, data: { orderId: validOrder.id, dbRecord } };
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// 4. Exhaustive Pattern Matching for HTTP Response
|
|
236
|
+
return match(pipelineResult, {
|
|
237
|
+
ok: (res) => ({ statusCode: 200, body: JSON.stringify(res) }),
|
|
238
|
+
err: (error) => {
|
|
239
|
+
switch (error._tag) {
|
|
240
|
+
case "ApiError":
|
|
241
|
+
return { statusCode: error.status, body: JSON.stringify({ error: error.message }) };
|
|
242
|
+
case "ValidationError":
|
|
243
|
+
return { statusCode: 400, body: JSON.stringify({ error: `Invalid ${error.field}: ${error.reason}` }) };
|
|
244
|
+
case "DbError":
|
|
245
|
+
return { statusCode: 500, body: JSON.stringify({ error: "Database transaction failed" }) };
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
132
252
|
---
|
|
133
253
|
|
|
134
254
|
## 🛡️ Built-in Safety Features
|
|
135
255
|
|
|
256
|
+
|
|
136
257
|
### Runtime Protection against Missing Asterisk (`*`)
|
|
137
258
|
|
|
138
259
|
Forgetting the asterisk (`yield` instead of `yield*`) is automatically intercepted at runtime and in TypeScript:
|
|
@@ -156,9 +277,9 @@ safe.sync(function* () {
|
|
|
156
277
|
* **`err(error)`** / **`Result.err(error)`** — Creates an `Err<E>` result.
|
|
157
278
|
* **`isOk(result)`** / **`isErr(result)`** — TypeScript type guards.
|
|
158
279
|
|
|
159
|
-
### Flow Runners (`safe`)
|
|
160
|
-
* **`safe.sync(function* () { ... })`** — Executes a synchronous generator function with early-return short-circuiting on `Err`.
|
|
161
|
-
* **`safe.async(async function* () { ... })`** — Executes an async generator function with early-return short-circuiting on `Err`.
|
|
280
|
+
### Flow Runners (`safe` & `Result.gen`)
|
|
281
|
+
* **`safe.sync(function* () { ... })`** / **`Result.gen(function* () { ... })`** — Executes a synchronous generator function with early-return short-circuiting on `Err`.
|
|
282
|
+
* **`safe.async(async function* () { ... })`** / **`Result.gen.async(async function* () { ... })`** — Executes an async generator function with early-return short-circuiting on `Err`.
|
|
162
283
|
|
|
163
284
|
### Exception Wrappers
|
|
164
285
|
* **`fromThrowable(fn, errorMapper?)`** — Wraps a throwing function into a `Result<T, E>`.
|
package/dist/types.d.ts
CHANGED
|
@@ -88,7 +88,17 @@ export declare const partition: <T, E>(results: Result<T, E>[]) => {
|
|
|
88
88
|
errors: E[];
|
|
89
89
|
};
|
|
90
90
|
/**
|
|
91
|
-
*
|
|
91
|
+
* Interface representing a discriminated error object with a `_tag` property.
|
|
92
|
+
*/
|
|
93
|
+
export interface TaggedError<T extends string> {
|
|
94
|
+
readonly _tag: T;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Creates an immutable TaggedError object with a `_tag` property and optional payload properties.
|
|
98
|
+
*/
|
|
99
|
+
export declare const taggedError: <T extends string, P extends Record<string, unknown> = {}>(tag: T, props?: P) => Readonly<TaggedError<T> & P>;
|
|
100
|
+
/**
|
|
101
|
+
* Namespace object grouping all Result constructors, guards, combinators, and generator runners.
|
|
92
102
|
*/
|
|
93
103
|
export declare const Result: {
|
|
94
104
|
readonly ok: <T>(value: T) => Ok<T>;
|
|
@@ -112,4 +122,9 @@ export declare const Result: {
|
|
|
112
122
|
values: T[];
|
|
113
123
|
errors: E[];
|
|
114
124
|
};
|
|
125
|
+
readonly taggedError: <T extends string, P extends Record<string, unknown> = {}>(tag: T, props?: P) => Readonly<TaggedError<T> & P>;
|
|
126
|
+
readonly gen: (<G extends Generator<Err<unknown>, unknown, unknown>>(fn: () => G) => Result<G extends Generator<unknown, infer R, unknown> ? R : G extends AsyncGenerator<unknown, infer R_1, unknown> ? R_1 : never, G extends Generator<infer Y, unknown, unknown> ? Y extends Err<infer E> ? E : never : G extends AsyncGenerator<infer Y_1, unknown, unknown> ? Y_1 extends Err<infer E_1> ? E_1 : never : never>) & {
|
|
127
|
+
sync: <G extends Generator<Err<unknown>, unknown, unknown>>(fn: () => G) => Result<G extends Generator<unknown, infer R, unknown> ? R : G extends AsyncGenerator<unknown, infer R_1, unknown> ? R_1 : never, G extends Generator<infer Y, unknown, unknown> ? Y extends Err<infer E> ? E : never : G extends AsyncGenerator<infer Y_1, unknown, unknown> ? Y_1 extends Err<infer E_1> ? E_1 : never : never>;
|
|
128
|
+
async: <G extends AsyncGenerator<Err<unknown>, unknown, unknown>>(fn: () => G) => Promise<Result<G extends Generator<unknown, infer R, unknown> ? R : G extends AsyncGenerator<unknown, infer R_1, unknown> ? R_1 : never, G extends Generator<infer Y, unknown, unknown> ? Y extends Err<infer E> ? E : never : G extends AsyncGenerator<infer Y_1, unknown, unknown> ? Y_1 extends Err<infer E_1> ? E_1 : never : never>>;
|
|
129
|
+
};
|
|
115
130
|
};
|
package/dist/types.js
CHANGED
|
@@ -134,7 +134,19 @@ export const partition = (results) => {
|
|
|
134
134
|
return { values, errors };
|
|
135
135
|
};
|
|
136
136
|
/**
|
|
137
|
-
*
|
|
137
|
+
* Creates an immutable TaggedError object with a `_tag` property and optional payload properties.
|
|
138
|
+
*/
|
|
139
|
+
export const taggedError = (tag, props) => Object.freeze({
|
|
140
|
+
_tag: tag,
|
|
141
|
+
...(props ?? {}),
|
|
142
|
+
});
|
|
143
|
+
import { safe } from "./flow.js";
|
|
144
|
+
const genRunner = Object.assign((fn) => safe.sync(fn), {
|
|
145
|
+
sync: safe.sync,
|
|
146
|
+
async: safe.async,
|
|
147
|
+
});
|
|
148
|
+
/**
|
|
149
|
+
* Namespace object grouping all Result constructors, guards, combinators, and generator runners.
|
|
138
150
|
*/
|
|
139
151
|
export const Result = {
|
|
140
152
|
ok,
|
|
@@ -152,4 +164,6 @@ export const Result = {
|
|
|
152
164
|
match,
|
|
153
165
|
all,
|
|
154
166
|
partition,
|
|
167
|
+
taggedError,
|
|
168
|
+
gen: genRunner,
|
|
155
169
|
};
|
package/package.json
CHANGED