slang-ts 0.2.1 → 0.2.3

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/CHANGELOG.md CHANGED
@@ -1,13 +1,22 @@
1
1
  # Changelog
2
2
 
3
- Everything notable in `slang-ts`, grouped by area. No release diary — just what's here.
3
+ Everything notable in `slang-ts`, grouped by area. No release diary: just what's here.
4
4
 
5
5
  ## Results and flow
6
6
 
7
- - `Ok(value, message?)` keeps an optional success message on the result, like text you want to show
8
- the user. Results without one have no `message` property.
7
+ - `Ok(value, message?)` keeps an optional success message on the result. Results without one have no `message` property.
9
8
  - `SafeTry` unwraps Option and Atom values into one result. A Result never nests inside another
10
9
  Result, and plain object properties and array elements stay unchanged.
10
+ - `safeTry` is sync in, sync out: a function returning a plain value gets its `Result`
11
+ directly, and a function returning a promise gets a promise of it. With `{ throw: true }`, a
12
+ sync throw is thrown from the call and an async one rejects.
13
+ - `Result.mapErr(fn)` rewrites an `Err` message and keeps its `.raw` cause; `Result.orFn(fn)`
14
+ recovers from an `Err` with the same return rules as `andThen`. Both return the same `Ok`
15
+ untouched.
16
+ - `andThen` and `orFn` on a `Result` return a plain `Result` for a sync callback; an async
17
+ callback gives `Result | Promise<Result>` to `await`.
18
+ - `combineResults` turns an array of `Result`s into one `Ok` of all values, typed as a tuple,
19
+ or the first `Err` with its `.raw` kept. `combineErrors` lists every error message in order.
11
20
 
12
21
  ## Collections and data
13
22
 
@@ -33,7 +42,8 @@ Everything notable in `slang-ts`, grouped by area. No release diary — just wha
33
42
 
34
43
  - `createStepper`: bounded step history with named steps, undo, and explicit `prev` handling.
35
44
  - `println` respects the environment: `setEnvironment("production")` turns it into a no-op, and
36
- `{ printFn }` routes output through an existing logger.
45
+ `{ printFn }` routes output through an existing logger. Plain objects, arrays, Sets and Maps
46
+ print as JSON, circular references included; errors and dates keep their text form.
37
47
 
38
48
  ## Typing and packaging
39
49
 
@@ -49,5 +59,7 @@ Everything notable in `slang-ts`, grouped by area. No release diary — just wha
49
59
 
50
60
  ## Documentation
51
61
 
52
- - A shorter README tour, per-utility reference pages, and plain-TypeScript contrasts for the
53
- utilities that replace real boilerplate.
62
+ - A shorter README tour and per-utility reference pages. Each page explains how the utility
63
+ behaves (when it runs, what it returns, how it fails) and every example compiles and runs
64
+ as written.
65
+ - An agent guide at `skill/SKILL.md`, shipped with the package.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  An opinionated functional programming library for TypeScript.
7
7
 
8
8
  A collection of utilities and mechanisms brought over from languages like Rust and Erlang. Each
9
- utility is deliberately chosen and built from scratch with zero dependencies.
9
+ utility is deliberately selected and built from scratch with zero dependencies.
10
10
 
11
11
  ## Install
12
12
 
@@ -20,26 +20,30 @@ Well I love functional programming so much and hate classes and inheritance with
20
20
 
21
21
  ## Quick start
22
22
 
23
- So let's say you have a function that can fail. Instead of throwing and hoping someone up the chain
24
- catches it, you return a `Result`:
23
+ So let's say your function can fail. Instead of throwing, you return a `Result`: `Ok` with the
24
+ value or `Err` with a message, and the caller checks `isOk` before reading either:
25
25
 
26
26
  ```ts
27
27
  import { Ok, Err, type Result, println } from "slang-ts";
28
28
 
29
- function divide(a: number, b: number): Result<number, string> {
30
- return b === 0 ? Err("Cannot divide by zero") : Ok(a / b);
29
+ function splitBill(total: number, people: number): Result<number, string> {
30
+ if (people === 0) {
31
+ return Err("Add at least one person to split the bill");
32
+ }
33
+
34
+ return Ok(total / people);
31
35
  }
32
36
 
33
- const result = divide(10, 2);
37
+ const share = splitBill(120, 4);
34
38
 
35
- if (result.isOk) {
36
- println("Result:", result.value); // 5
39
+ if (share.isOk) {
40
+ println("Each person pays:", share.value); // 30
37
41
  } else {
38
- println("Error:", result.error);
42
+ println("Could not split the bill:", share.error);
39
43
  }
40
44
  ```
41
45
 
42
- `println` is optional — it's a small development logger.
46
+ `println` is optional: it's a small development logger, silent in production.
43
47
 
44
48
  ## Imports
45
49
 
@@ -49,145 +53,145 @@ Import only what you use:
49
53
  import { option, Ok, Err, atom, match } from "slang-ts";
50
54
  ```
51
55
 
52
- ## Choose by problem
56
+ ## Choose by what you want to solve
53
57
 
54
58
  | If you need… | Use |
55
59
  |---|---|
56
- | A value that may be missing | [Option][option] |
57
- | An operation that can fail | [Result][result] and [SafeTry][safe-try] |
60
+ | A value which may be missing | [Option][option] |
61
+ | An operation which can fail | [Result][result] and [SafeTry][safe-try] |
62
+ | Several results checked together | [combineResults and combineErrors][combine] |
58
63
  | Pattern matching over results | [Match][match] |
64
+ | Steps which stop at the first failure | [Pipe][pipe] |
59
65
  | Timeout, retry, or cancellation | [SuperPromise][super-promise] |
60
66
  | Ordered, parallel, or race tasks | [SuperRunner][super-runner] |
61
67
  | A shared concurrency limit | [taskGroup][task-group] |
62
68
  | Strict array bounds or paging | [list][list] |
63
- | Safe nested record access | [struct][struct] |
64
- | Set membership and algebra | [sets][sets] |
69
+ | Nested record reads with defaults | [struct][struct] |
70
+ | Set membership and set operations | [sets][sets] |
65
71
  | Lazy pipelines over an `Ok` | [Ok.iterate][iterate] |
66
72
  | Cached function calls | [memoized][memoized] |
67
73
  | Step history or undo | [createStepper][stepper] |
68
- | Publish/subscribe between parts of an app | [Channel][channel] |
69
- | A lifecycle trigger that fires once | [Signal][signal] |
74
+ | Events between parts of an app | [Channel][channel] |
75
+ | A lifecycle trigger which fires once | [Signal][signal] |
70
76
 
71
77
  ## What's inside
72
78
 
73
79
  ### Error handling and flow
74
80
 
75
- - [Option][option] (Some/None, `isFalsy`, expect, unwrap/else, andThen)
76
- - [Result][result] (Ok/Err, expect, unwrap/else, andThen, raw cause)
77
- - [SafeTry][safe-try]
78
- - [Match][match] (match, matchAll)
79
- - [Options][options]
80
- - [Pipe][pipe]
81
+ - [Result][result] (`Ok`, `Err`, expect, unwrap with else, andThen, mapErr, orFn, raw cause)
82
+ - [SafeTry][safe-try] (`safeTry`)
83
+ - [combineResults and combineErrors][combine] (`combineResults`, `combineErrors`)
84
+ - [Option][option] (`option`, Some, None, `isFalsy`, expect, unwrap with else, andThen)
85
+ - [Match][match] (`match`, `matchAll`)
86
+ - [Options][options] (`Options`)
87
+ - [Pipe][pipe] (`pipe`)
88
+ - [Panic][panic] (`panic`)
81
89
 
82
90
  ### Composition and conversion
83
91
 
84
- - [Atom][atom]
85
- - [To][to] (`.to()` converters)
86
- - [Zip][zip] (zip, zipWith, unzip)
92
+ - [Atom][atom] (`atom`)
93
+ - [To][to] (`.to()` on every wrapper, and `_to`)
94
+ - [Zip][zip] (`zip`, `zipWith`, `unzip`)
87
95
 
88
96
  ### Collections and data
89
97
 
90
- - [list][list]
91
- - [struct][struct]
92
- - [sets][sets]
93
- - [Ok.iterate][iterate]
94
- - [memoized][memoized]
98
+ - [list][list] (`list`)
99
+ - [struct][struct] (`struct`)
100
+ - [sets][sets] (`sets`)
101
+ - [Ok.iterate][iterate] (`Ok(value).iterate()`)
102
+ - [memoized][memoized] (`memoized`)
95
103
 
96
104
  ### Async and lifecycle
97
105
 
98
- - [SuperPromise][super-promise]
99
- - [SuperRunner][super-runner]
100
- - [taskGroup][task-group]
101
- - [Signal][signal]
102
- - [Channel][channel]
106
+ - [SuperPromise][super-promise] (`superPromise`)
107
+ - [SuperRunner][super-runner] (`superRunner`: sequence, all, race)
108
+ - [taskGroup][task-group] (`taskGroup`)
109
+ - [Signal][signal] (`createSignal`)
110
+ - [Channel][channel] (`createChannel`: send, subscribe, when)
111
+ - [createStepper][stepper] (`createStepper`)
103
112
 
104
113
  ### Small utilities
105
114
 
106
- - [Panic][panic]
107
- - [println][logging] (with `setEnvironment`)
108
- - [createStepper][stepper]
115
+ - [println][logging] (`println`, `setEnvironment`)
109
116
 
110
- Full reference: [docs index][docs-index]. Feature list: [CHANGELOG.md][changelog]. Agent guide: [skill/SKILL.md](skill/SKILL.md).
117
+ Full reference: [docs index][docs-index]. Feature list: [CHANGELOG.md][changelog]. Agent guide: [skill/SKILL.md][skill].
111
118
 
112
119
  ## Real-world examples
113
120
 
114
- ### Load a value that can fail: SafeTry + Result
121
+ ### Load a value which can fail: SafeTry + Result
115
122
 
116
- The edge of your app probably mixes failures: a lookup that throws, a parser that rejects a value,
117
- an optional result. `safeTry` brings all of that into one `Result`:
123
+ Loading saved settings can fail two ways: the saved text is not valid JSON, or the role inside
124
+ is one you do not support. `safeTry` turns both into one `Result`:
118
125
 
119
126
  ```ts
120
- import { Err, Ok, safeTry, type Result, println } from "slang-ts";
127
+ import { Ok, Err, safeTry, type Result, println } from "slang-ts";
121
128
 
122
129
  type Role = "admin" | "member";
123
130
 
131
+ type SavedSettings = {
132
+ role: string;
133
+ };
134
+
124
135
  function parseRole(value: string): Result<Role, string> {
125
- return value === "admin" || value === "member"
126
- ? Ok(value)
127
- : Err("Unknown role");
136
+ if (value === "admin" || value === "member") {
137
+ return Ok(value);
138
+ }
139
+
140
+ return Err("Unknown role");
128
141
  }
129
142
 
130
- const role = await safeTry(() => {
131
- const saved = localStorage.getItem("role");
132
- if (saved === null) throw new Error("No role saved");
133
- return parseRole(saved);
143
+ const savedText = '{"role":"admin"}';
144
+
145
+ const role = safeTry(() => {
146
+ const settings = JSON.parse(savedText) as SavedSettings;
147
+
148
+ return parseRole(settings.role);
134
149
  });
135
150
 
136
151
  if (role.isOk) {
137
- println("Role:", role.value);
152
+ println("Role:", role.value); // "admin"
138
153
  } else {
139
- println("Could not load role:", role.error);
154
+ println("Could not load the role:", role.error);
140
155
  }
141
156
  ```
142
157
 
143
- Without it, you'd have to catch the throw and still branch on the value:
144
-
145
- ```ts
146
- type Role = "admin" | "member";
147
-
148
- function loadRole(): Role {
149
- const saved = localStorage.getItem("role");
150
- if (saved === null) throw new Error("No role saved");
151
- if (saved !== "admin" && saved !== "member") throw new Error("Unknown role");
152
- return saved;
153
- }
154
-
155
- try {
156
- const role = loadRole();
157
- console.log("Role:", role);
158
- } catch (error) {
159
- console.log(
160
- "Could not load role:",
161
- error instanceof Error ? error.message : "Unknown error",
162
- );
163
- }
164
- ```
158
+ A throw from `JSON.parse` and an `Err` from `parseRole` both come back as `Err`, so you check
159
+ the result once. `safeTry` itself never throws. Your function is sync here, so the `Result`
160
+ comes back directly; an async function gives you a promise of it to `await`.
165
161
 
166
162
  Full detail: [SafeTry][safe-try] and [Result][result].
167
163
 
168
164
  ### Call an API with a timeout and retries: SuperPromise
169
165
 
170
- Your function gets an `AbortSignal`. `timeout` aborts an attempt, `retry` runs it again after an
171
- ordinary failure, and you get back `Ok` or `Err` — so no `try`/`catch` around the await.
166
+ Your function receives an `AbortSignal` to pass along to `fetch`. `timeout` stops an attempt
167
+ once it takes too long, `retry` runs it again after a failure, and the call resolves to `Ok` or
168
+ `Err`. It never rejects:
172
169
 
173
170
  ```ts
174
171
  import { superPromise, println } from "slang-ts";
175
172
 
176
- const id = "123";
173
+ type User = {
174
+ id: string;
175
+ name: string;
176
+ };
177
177
 
178
178
  const user = await superPromise(
179
- async (signal) => {
180
- const response = await fetch(`/api/users/${id}`, { signal });
181
- if (!response.ok) throw new Error("User not found");
182
- return (await response.json()) as { name: string };
179
+ async (signal: AbortSignal): Promise<User> => {
180
+ const response = await fetch("https://example.com/api/users/u_1", { signal });
181
+
182
+ if (!response.ok) {
183
+ throw new Error("Could not load the user");
184
+ }
185
+
186
+ return (await response.json()) as User;
183
187
  },
184
- { timeout: 5000, retry: 2 },
188
+ { timeout: 5000, retry: 2 }, // 5 seconds per attempt, up to 2 retries
185
189
  );
186
190
 
187
191
  if (user.isOk) {
188
- println("User:", user.value.name);
192
+ println("User:", user.value.name); // "Ada"
189
193
  } else {
190
- println("Could not load user:", user.error);
194
+ println("Could not load the user:", user.error);
191
195
  }
192
196
  ```
193
197
 
@@ -195,8 +199,8 @@ Full detail: [SuperPromise][super-promise].
195
199
 
196
200
  ### Page a list and shape records: list + struct
197
201
 
198
- `list` keeps bounds strict: a bad range fails instead of quietly giving you less data. `struct`
199
- reads and shapes records without spreading fields you didn't choose.
202
+ `list` keeps bounds strict: a range outside the array fails instead of returning fewer items. `struct`
203
+ reads and shapes records, keeping only the fields you choose.
200
204
 
201
205
  ```ts
202
206
  import { list, struct, println } from "slang-ts";
@@ -208,25 +212,28 @@ type User = {
208
212
  email: string;
209
213
  };
210
214
 
211
- const users: User[] = await getUsers(); // your data source
215
+ const users: User[] = [
216
+ { id: "u_1", name: "Ada", avatar: "/avatars/ada.png", email: "ada@example.com" },
217
+ { id: "u_2", name: "Grace", email: "grace@example.com" },
218
+ { id: "u_3", name: "Linus", email: "linus@example.com" },
219
+ ];
220
+
221
+ const page = 1;
222
+ const size = 2;
212
223
 
213
- const page = 2;
214
- const size = 20;
215
224
  const total = list(users).count().else(0);
216
225
  const start = Math.min(page * size, total);
217
- const paged = list(users)
218
- .range(start, Math.min(start + size, total))
219
- .else([]);
226
+ const end = Math.min(start + size, total);
220
227
 
221
- const cards = paged.map((user) =>
222
- struct(user).pick("id", "name", "avatar").else(user),
223
- );
228
+ const paged = list(users).range(start, end).else([]);
229
+
230
+ const cards = paged.map((user) => struct(user).pick("id", "name", "avatar").else(user));
224
231
 
225
- println(cards);
232
+ println(cards); // [{"id":"u_3","name":"Linus"}]
226
233
  ```
227
234
 
228
- `list` won't clamp a range for you, so the example clamps the window and lets the last page be
229
- short. `struct` then keeps only the fields the card needs.
235
+ `list` never shortens a range, so the example clamps the window and lets the last page
236
+ be short. `struct` then keeps only the fields the card needs.
230
237
 
231
238
  Full detail: [list][list] and [struct][struct].
232
239
 
@@ -248,7 +255,7 @@ projects using Slang.
248
255
 
249
256
  ## Contributing
250
257
 
251
- Got a utility from another language that belongs here? Open an issue or a pull request.
258
+ Got a utility from another language which belongs here? Open an issue or a pull request.
252
259
  Contributions are welcome.
253
260
 
254
261
  ## Requirements
@@ -265,6 +272,7 @@ MIT License. See [LICENSE][license].
265
272
  [option]: https://github.com/Hussseinkizz/slang/blob/main/docs/option.md
266
273
  [result]: https://github.com/Hussseinkizz/slang/blob/main/docs/result.md
267
274
  [safe-try]: https://github.com/Hussseinkizz/slang/blob/main/docs/safe-try.md
275
+ [combine]: https://github.com/Hussseinkizz/slang/blob/main/docs/combine.md
268
276
  [match]: https://github.com/Hussseinkizz/slang/blob/main/docs/match.md
269
277
  [options]: https://github.com/Hussseinkizz/slang/blob/main/docs/match.md#options
270
278
  [pipe]: https://github.com/Hussseinkizz/slang/blob/main/docs/pipe.md
package/dist/index.d.ts CHANGED
@@ -116,8 +116,52 @@ interface ResultMethods<T> {
116
116
  * Ok(5).andThen(x => undefined); // Ok(5) - original
117
117
  * await Ok(5).andThen(async x => x); // Ok(5)
118
118
  */
119
+ andThen<R extends PromiseLike<unknown>>(fn: (value: T) => R, ..._asyncOnly: AsyncCallbackGuard<R>): Result<SettledValue<Awaited<R>>, string> | Promise<Result<SettledValue<Awaited<R>>, string>>;
120
+ /**
121
+ * Sync callback overload: the result is a plain Result, so it can be read without `await`.
122
+ * The async overload above stays a union because a skipped `Err` returns itself
123
+ * synchronously; the callback never runs, so nothing can tell it was async.
124
+ */
125
+ andThen<R>(fn: (value: T) => R, ..._syncOnly: SyncCallbackGuard<R>): Result<SettledValue<R>, string>;
126
+ /**
127
+ * Fallback for a callback that may or may not return a promise, such as
128
+ * `flag ? value : Promise.resolve(value)`, or whose return is `unknown`. Typed as the
129
+ * union so the promise case is not hidden.
130
+ */
119
131
  andThen<U>(fn: (value: T) => U | Result<U, string> | Promise<U | Result<U, string>>): Result<U, string> | Promise<Result<U, string>>;
132
+ /**
133
+ * Rewrites the error message if `Err`, returns the same `Ok` if `Ok`.
134
+ * Exists so a technical message can be made human at a boundary while `.raw` keeps the
135
+ * cause for logs. Sync only: rewriting a message never needs to wait.
136
+ * - If fn throws, returns `Err(message)` with the original `.raw`.
137
+ * @example
138
+ * Err("ECONNRESET", cause).mapErr(() => "Could not reach the server"); // .raw is still cause
139
+ * Ok(5).mapErr(() => "never runs"); // same Ok
140
+ */
141
+ mapErr(fn: (error: string) => string): Result<T, string>;
142
+ /**
143
+ * Recovers from `Err`, returns the same `Ok` if `Ok`. The error-side mirror of `andThen`,
144
+ * named `orFn` so it is not confused with `unwrap().else()`.
145
+ * - `raw` is passed so recovery can inspect the cause, such as retrying only on a network failure.
146
+ * - A plain value becomes `Ok(value)`; a returned Result is passed through.
147
+ * - If fn returns `undefined`, returns the original `Err`.
148
+ * - If fn throws or rejects, returns `Err(message)`.
149
+ * @example
150
+ * Err("missing").orFn(() => 0); // Ok(0)
151
+ * Err("missing").orFn((e) => Err(`${e}!`)); // Err("missing!")
152
+ * await Err("offline").orFn(async () => cached); // Ok(cached)
153
+ */
154
+ orFn<R extends PromiseLike<unknown>>(fn: (error: string, raw: unknown) => R, ..._asyncOnly: AsyncCallbackGuard<R>): Result<T | SettledValue<Awaited<R>>, string> | Promise<Result<T | SettledValue<Awaited<R>>, string>>;
155
+ /** Sync callback overload: the result is a plain Result, so it can be read without `await`. */
156
+ orFn<R>(fn: (error: string, raw: unknown) => R, ..._syncOnly: SyncCallbackGuard<R>): Result<T | SettledValue<R>, string>;
157
+ /**
158
+ * Fallback for a callback that may or may not return a promise, or whose return is
159
+ * `unknown`. Typed as the union so the promise case is not hidden.
160
+ */
161
+ orFn<U>(fn: (error: string, raw: unknown) => U | Result<U, string> | Promise<U | Result<U, string>>): Result<T | U, string> | Promise<Result<T | U, string>>;
120
162
  }
163
+ /** The value a callback's return settles to: a returned Result passes its value through. */
164
+ type SettledValue<R> = R extends ResultMethods<infer Value> ? Value : R;
121
165
  /**
122
166
  * Creates a new, successful result.
123
167
  * @param value Value of the result. A Result is rejected; a Result never nests inside another Result.
@@ -236,15 +280,31 @@ interface OptionMethods<T> {
236
280
  };
237
281
  /**
238
282
  * Transforms the inner value if `Some`, returns `None` if `None`.
283
+ * - A sync callback returns a plain Option, readable without `await`.
284
+ * - An async callback returns `Option | Promise<Option>`: a `None` skips the callback and
285
+ * returns itself synchronously, so `await` the result.
286
+ * - The returned value is wrapped with `option()`, not flattened: returning an Option gives
287
+ * `Some(thatOption)`, and a falsy value gives `None`.
239
288
  * - If fn returns `undefined`, returns original Option (no new allocation).
240
- * - If fn throws, returns `None`.
241
- * - Supports async functions, returning `Promise<Option<U>>`.
289
+ * - If fn throws or rejects, returns `None`.
242
290
  * @example
243
291
  * option(5).andThen(x => x * 2); // Some(10)
244
292
  * option(null).andThen(x => x * 2); // None
245
293
  * option(5).andThen(x => undefined); // Some(5) - original
246
294
  * await option(5).andThen(async x => x); // Some(5)
247
295
  */
296
+ andThen<R extends PromiseLike<unknown>>(fn: (value: T) => R, ..._asyncOnly: AsyncCallbackGuard<R>): Option<Awaited<R>> | Promise<Option<Awaited<R>>>;
297
+ /**
298
+ * Sync callback overload: the result is a plain Option, so it can be read without `await`.
299
+ * The async overload above stays a union because a `None` returns itself synchronously;
300
+ * the callback never runs, so nothing can tell it was async.
301
+ */
302
+ andThen<R>(fn: (value: T) => R, ..._syncOnly: SyncCallbackGuard<R>): Option<R>;
303
+ /**
304
+ * Fallback for a callback that may or may not return a promise, such as
305
+ * `flag ? value : Promise.resolve(value)`, or whose return is `unknown`. Typed as the
306
+ * union so the promise case is not hidden.
307
+ */
248
308
  andThen<U>(fn: (value: T) => U | Promise<U>): Option<U> | Promise<Option<U>>;
249
309
  }
250
310
  /**
@@ -281,6 +341,20 @@ export declare function option<T>(value: T | NonTruthy): Option<T> & OptionMetho
281
341
  * Used internally for deferred error handling in unwrap chains.
282
342
  */
283
343
  export declare const scheduleMicrotask: (fn: () => void) => void;
344
+ /** Evaluated value result type */
345
+ /**
346
+ * Rest-parameter guard for a sync-callback overload: matches only callbacks whose return
347
+ * type is known to hold no promise. `unknown`, `any` and promise unions fall through to the
348
+ * union overload, because at runtime they may return either shape. `Rest` lets a caller add
349
+ * its own guard once the callback is known to be sync.
350
+ */
351
+ type SyncCallbackGuard<T, Rest extends readonly string[] = []> = unknown extends T ? [never] : [Extract<T, PromiseLike<unknown>>] extends [never] ? Rest : [never];
352
+ /**
353
+ * Rest-parameter guard for a promise-callback overload: matches only real promise returns.
354
+ * A callback that only throws returns `never`, which is assignable to a promise type but
355
+ * runs synchronously, so it is sent to the sync overload.
356
+ */
357
+ type AsyncCallbackGuard<R, Rest extends readonly string[] = []> = [R] extends [never] ? [never] : Rest;
284
358
  //#endregion
285
359
  //#region src/println.d.ts
286
360
  type EnvironmentMode = "development" | "production" | "dev" | "prod";
@@ -293,12 +367,12 @@ type EnvironmentOptions = {
293
367
  * Safe to call any number of times, in any order; the last call wins.
294
368
  * Accepts a mode (full words or short forms: "development"/"dev", "production"/"prod")
295
369
  * or options `{ printFn }` to route println through an existing logger instead of
296
- * the console — useful in production, where logs go somewhere real.
370
+ * the console: useful in production, where logs go somewhere real.
297
371
  * Pass `{ printFn: null }` to switch back to the console.
298
372
  */
299
373
  export declare const setEnvironment: (next: EnvironmentMode | EnvironmentOptions) => void;
300
374
  /**
301
- * Prints a line to the console — args joined with spaces, newline appended.
375
+ * Prints a line to the console: args joined with spaces, newline appended.
302
376
  * When a custom printFn is set, the line goes to it instead, in every mode.
303
377
  * Otherwise production mode is silent, so dev chatter never leaks to prod logs.
304
378
  */
@@ -317,6 +391,28 @@ export declare const println: (...args: unknown[]) => void;
317
391
  */
318
392
  export declare function panic(message: string): never;
319
393
  //#endregion
394
+ //#region src/combine.d.ts
395
+ /** Maps a tuple or array of Results to the tuple or array of their success values. */
396
+ type CombinedValues<R extends readonly Result<unknown, string>[]> = { -readonly [K in keyof R]: R[K] extends ResultMethods<infer Value> ? Value : never; };
397
+ /**
398
+ * Combines results you already have into one Result of all their values.
399
+ * Stops at the first failure and returns that same `Err` instance, so its `.raw` cause is
400
+ * kept. Sync by design: dependent steps belong in `pipe`, async tasks in `superRunner`.
401
+ * @example
402
+ * combineResults([Ok(1), Ok("a")]); // Ok([1, "a"]), typed Result<[number, string], string>
403
+ * combineResults([Ok(1), Err("bad")]); // the Err("bad") instance
404
+ * combineResults([]); // Ok([])
405
+ */
406
+ export declare function combineResults<R extends readonly Result<unknown, string>[]>(results: readonly [...R]): Result<CombinedValues<R>, string>;
407
+ /**
408
+ * Collects every error message, in input order, so a form can show all problems at once
409
+ * instead of only the first. Returns `[]` when every result is `Ok`.
410
+ * @example
411
+ * combineErrors([Err("Enter a valid email"), Ok("Ada"), Err("Name is required")]);
412
+ * // ["Enter a valid email", "Name is required"]
413
+ */
414
+ export declare function combineErrors(results: readonly Result<unknown, string>[]): string[];
415
+ //#endregion
320
416
  //#region src/options.d.ts
321
417
  /**
322
418
  * Resolves the usable inner type of a value.
@@ -339,8 +435,8 @@ type OptionsValue<T> = T extends {
339
435
  * Returns the first usable value from a list as an `Ok`, or the fallback
340
436
  * `Err` if none is usable.
341
437
  *
342
- * Normalizes mixed values — raw values, `Option`s, and `Result`s
343
- * from different sources — when all you care about is whether any of them
438
+ * Normalizes mixed values: raw values, `Option`s, and `Result`s
439
+ * from different sources: when all you care about is whether any of them
344
440
  * is usable. A value is usable when its unwrapped inner value is truthy;
345
441
  * `None`, `Err`, null, undefined, `""`, NaN, and Infinity are skipped.
346
442
  * Note that `0` and `false` count as usable, just as they do in `option()`.
@@ -375,7 +471,7 @@ export declare function _to<E = string>(value: Atom<any>, target: "result"): Res
375
471
  //#endregion
376
472
  //#region src/match.d.ts
377
473
  /**
378
- * Pattern matching for `Result` and `Option` — exhaustiveness enforced.
474
+ * Pattern matching for `Result` and `Option`: exhaustiveness enforced.
379
475
  *
380
476
  * Returns the value returned by the selected handler. If all handlers return
381
477
  * `Result` or `Option`, TypeScript will infer that automatically.
@@ -496,29 +592,34 @@ type SafeTryOptions = {
496
592
  };
497
593
  /**
498
594
  * Captures failures and resolves nested Slang wrappers into one Result.
499
- * - Awaits the callback, then iteratively unwraps Result, Option, and Atom values.
595
+ * - A sync callback gets its Result back directly; a promise-returning callback
596
+ * gets a Promise of the Result. `await` works on both, so callers need not know.
597
+ * - Iteratively unwraps Result, Option, and Atom values.
500
598
  * - Returns the first Err or None as Err; cycles also return Err.
501
599
  * - Leaves object properties, array elements, and wrapped Promise values intact.
502
- * - `{ throw: true }` re-throws thrown errors; returned failures remain Err.
600
+ * - `{ throw: true }` re-throws thrown errors (synchronously for a sync callback,
601
+ * as a rejection for an async one); returned failures remain Err.
503
602
  * - `{ collectValues: true }` records every unwrapped layer and failure on
504
603
  * `result.raw` as `{ errors, values }`.
505
604
  * - A caught throw reaches the caller as `Err(message)`, with the thrown value on `.raw`.
506
605
  *
507
606
  * @param fn - Function to execute (sync or async)
508
607
  * @param options - `{ throw?: boolean; collectValues?: boolean }`
509
- * @returns Promise of one Result containing the final success value or error text
608
+ * @returns One Result for a sync callback, a Promise of one Result for an async one
510
609
  *
511
610
  * @example
512
- * const result = await safeTry(() => Ok(option("Hello")));
611
+ * const result = safeTry(() => Ok(option("Hello")));
513
612
  * if (result.isOk) println(result.value); // "Hello"
514
613
  *
515
614
  * @example
516
- * const result = await safeTry(() => {
615
+ * const result = await safeTry(async () => {
517
616
  * throw new Error("Oops!");
518
617
  * });
519
618
  * if (result.isErr) println(result.error);
520
619
  */
521
- export declare function safeTry<T>(fn: () => T | Promise<T>, options?: SafeTryOptions, ..._noNestedResult: NestedValueGuard<Awaited<T>>): Promise<Result<SafeTryValue<Awaited<T>>, string>>;
620
+ export declare function safeTry<R extends PromiseLike<unknown>>(fn: () => R, options?: SafeTryOptions, ..._noNestedResult: AsyncCallbackGuard<R, NestedValueGuard<Awaited<R>>>): Promise<Result<SafeTryValue<Awaited<R>>, string>>;
621
+ export declare function safeTry<T>(fn: () => T, options?: SafeTryOptions, ..._noNestedResult: SyncCallbackGuard<T, NestedValueGuard<T>>): Result<SafeTryValue<T>, string>;
622
+ export declare function safeTry<T>(fn: () => T | PromiseLike<T>, options?: SafeTryOptions, ..._noNestedResult: NestedValueGuard<T>): Result<SafeTryValue<T>, string> | Promise<Result<SafeTryValue<T>, string>>;
522
623
  //#endregion
523
624
  //#region src/list.d.ts
524
625
  /** Accepted list input: a raw array, or a narrowed successful `Ok` holding one. */
@@ -773,7 +874,7 @@ export declare function pipe<T, E = string>(initial: T, ...fns: PipeFn[]): Pipel
773
874
  type Executor<T> = (signal: AbortSignal) => T | Promise<T> | Result<T, string> | Promise<Result<T, string>>;
774
875
  /** Stop signal for `retryUntil`. */
775
876
  type RetryUntilOptions = {
776
- /** When this fires, retrying ends — mid-attempt firings cancel the in-flight attempt. */
877
+ /** When this fires, retrying ends: mid-attempt firings cancel the in-flight attempt. */
777
878
  signal: AbortSignal;
778
879
  /** Wait between attempts. No wait applies before the first attempt. */
779
880
  delay?: number;
@@ -786,7 +887,7 @@ type SuperPromiseOptions = {
786
887
  delay?: number;
787
888
  /** Abort the operation after this many milliseconds. Timeouts are never retried. */
788
889
  timeout?: number;
789
- /** `n` retries after the first attempt — `n + 1` attempts total. Aborts and timeouts are never retried. */
890
+ /** `n` retries after the first attempt: `n + 1` attempts total. Aborts and timeouts are never retried. */
790
891
  retry?: number;
791
892
  /** Keep retrying until the stop signal fires. Overrides `retry` and `delay`. */
792
893
  retryUntil?: RetryUntilOptions;
@@ -807,13 +908,13 @@ type SuperPromise<T> = {
807
908
  tap(fn: (value: T) => unknown | Promise<unknown>): SuperPromise<T>;
808
909
  /** Promise-compatible. The callback receives the settled `Result`; the task never rejects. */
809
910
  then<TR1 = Result<T, string>, TR2 = never>(onfulfilled?: ((value: Result<T, string>) => TR1 | PromiseLike<TR1>) | null, onrejected?: ((reason: unknown) => TR2 | PromiseLike<TR2>) | null): Promise<TR1 | TR2>;
810
- /** Promise-compatible. Effectively dead — failures are captured into `Err`, never rejections. */
911
+ /** Promise-compatible. Effectively dead: failures are captured into `Err`, never rejections. */
811
912
  catch<TR = never>(onrejected?: ((reason: unknown) => TR | PromiseLike<TR>) | null): Promise<Result<T, string> | TR>;
812
913
  /** Promise-compatible. Native behavior. */
813
914
  finally(onfinally?: (() => void) | null): Promise<Result<T, string>>;
814
915
  };
815
916
  /**
816
- * Creates a SuperPromise — one async operation plus start, retry, timeout, and cancel controls.
917
+ * Creates a SuperPromise: one async operation plus start, retry, timeout, and cancel controls.
817
918
  *
818
919
  * Lazy start: your function runs on first use (`await`/`.then()`, or a runner's `start()`),
819
920
  * not at creation. Defer chains start recursively, so awaiting a deferred task runs its
@@ -891,7 +992,7 @@ type Channel = {
891
992
  }): void;
892
993
  };
893
994
  /**
894
- * Creates a Channel — completely independent of SuperPromise/SuperRunner.
995
+ * Creates a Channel: completely independent of SuperPromise/SuperRunner.
895
996
  *
896
997
  * @example
897
998
  * const channel = createChannel();
@@ -907,7 +1008,7 @@ export declare function createChannel(): Channel;
907
1008
  //#endregion
908
1009
  //#region src/signal.d.ts
909
1010
  /**
910
- * A lifecycle Signal — a trigger that fires once and stays fired.
1011
+ * A lifecycle Signal: a trigger that fires once and stays fired.
911
1012
  *
912
1013
  * ... structurally an `AbortSignal` plus a `fire()` method,
913
1014
  * so it can be passed anywhere a native `AbortSignal` is expected
@@ -933,9 +1034,9 @@ type Signal = AbortSignal & {
933
1034
  * signal.aborted; // true
934
1035
  * ```
935
1036
  *
936
- * Listeners are NOT replayed after firing — consumers must check
1037
+ * Listeners are NOT replayed after firing: consumers must check
937
1038
  * `signal.aborted` synchronously to detect an already-fired signal.
938
1039
  */
939
1040
  export declare function createSignal(): Signal;
940
1041
  //#endregion
941
- export type { Atom, AtomMethods, Channel, ChannelEvent, ChannelSubscriber, EnvironmentMode, EnvironmentOptions, Err as ErrType, Executor, Fallible, IterateChain, ListChain, ListGet, ListInput, Memoized, NextOptions, NonTruthy, None, Ok as OkType, Option, OptionMethods, OptionsValue, PipeEachContext, PipeFn, PipeRunOptions, Pipeline, PrevRecover, PrevResolution, PrintFn, Result, ResultMethods, RetryUntilOptions, SetsChain, SetsInput, Signal, SlangErrorCategory, Some, Step, StepFactory, StepFn, Stepper, StepperConfig, StructChain, StructGet, SuperPromise, SuperPromiseOptions, SuperRunner, TaskGroup, TaskGroupOptions };
1042
+ export type { Atom, AtomMethods, Channel, ChannelEvent, ChannelSubscriber, CombinedValues, EnvironmentMode, EnvironmentOptions, Err as ErrType, Executor, Fallible, IterateChain, ListChain, ListGet, ListInput, Memoized, NextOptions, NonTruthy, None, Ok as OkType, Option, OptionMethods, OptionsValue, PipeEachContext, PipeFn, PipeRunOptions, Pipeline, PrevRecover, PrevResolution, PrintFn, Result, ResultMethods, RetryUntilOptions, SetsChain, SetsInput, Signal, SlangErrorCategory, Some, Step, StepFactory, StepFn, Stepper, StepperConfig, StructChain, StructGet, SuperPromise, SuperPromiseOptions, SuperRunner, TaskGroup, TaskGroupOptions };