Skip to content

Commit 8df8832

Browse files
authored
Merge pull request #2 from Noctua-Technology/feature/all
feat: Result.all
2 parents 18d93cb + 920cf77 commit 8df8832

6 files changed

Lines changed: 140 additions & 133 deletions

File tree

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,23 @@ Notes:
145145
- `backoff` increases wait time after each retry
146146
- final failed result is marked with `attempted = true`
147147

148+
## Collecting multiple results
149+
150+
Use `Result.all` to combine an array (or tuple) of Results into a single Result. It returns `Ok` with all values if every result succeeded, or the first `Err` it encounters.
151+
152+
```ts
153+
const results = Result.all([
154+
Result.wrap(() => JSON.parse('{"port":3000}')),
155+
Result.wrap(() => JSON.parse('{"host":"localhost"}')),
156+
]);
157+
158+
if (results.ok) {
159+
const [portConfig, hostConfig] = results.val;
160+
}
161+
```
162+
163+
Tuple types are fully preserved, so each element's type is inferred independently.
164+
148165
## Runtime checks
149166

150167
```ts
@@ -163,6 +180,7 @@ if (Result.isResult(maybe)) {
163180
- Read values: `unwrap`, `unwrapOr` (value or function), `expect`, `expectErr`
164181
- Wrappers: `Result.wrap`, `Result.wrapAsync`
165182
- Retry: `Result.attempt`
183+
- Collect: `Result.all`
166184
- Type guard: `Result.isResult`
167185

168186
## Development

package-lock.json

Lines changed: 17 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@
2525
]
2626
},
2727
"test": {
28-
"command": "node --test \"./target/**/*.test.js\"",
28+
"command": "node --enable-source-maps --test \"./target/**/*.test.js\"",
2929
"dependencies": [
3030
"build"
3131
]
3232
}
3333
},
3434
"devDependencies": {
3535
"@types/node": "^24.0.4",
36-
"typescript": "^5.9.3",
36+
"typescript": "^6.0.0",
3737
"wireit": "^0.14.12"
3838
}
3939
}

src/lib/result.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,45 @@ describe("Result", () => {
385385
});
386386
});
387387

388+
describe("Result.all", () => {
389+
it("should return Ok with all values when all results are Ok", () => {
390+
const result = Result.all([new Ok(1), new Ok(2), new Ok(3)]);
391+
392+
assert.ok(result instanceof Ok);
393+
assert.deepStrictEqual(result.val, [1, 2, 3]);
394+
});
395+
396+
it("should return the first Err when any result is Err", () => {
397+
const result = Result.all([new Ok(1), new Err("boom"), new Ok(3)]);
398+
399+
assert.ok(result instanceof Err);
400+
assert.strictEqual(result.val, "boom");
401+
});
402+
403+
it("should return the first Err and not continue evaluating", () => {
404+
const result = Result.all([new Err("first"), new Err("second")]);
405+
406+
assert.ok(result instanceof Err);
407+
assert.strictEqual(result.val, "first");
408+
});
409+
410+
it("should return Ok with an empty array for an empty input", () => {
411+
const result = Result.all([]);
412+
413+
assert.ok(result instanceof Ok);
414+
assert.deepStrictEqual(result.val, []);
415+
});
416+
417+
it("should preserve tuple types", () => {
418+
const a = new Ok(42) as Result<number, string>;
419+
const b = new Ok("hello") as Result<string, string>;
420+
const result = Result.all([a, b]);
421+
422+
assert.ok(result instanceof Ok);
423+
assert.deepStrictEqual(result.val, [42, "hello"]);
424+
});
425+
});
426+
388427
describe("Complex scenarios", () => {
389428
it("should handle chaining multiple operations", () => {
390429
const result = new Ok(5)

src/lib/result.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,7 @@ export class Err<E> implements BaseResult<never, E> {
8989
this.val = val;
9090

9191
const stackLines = new Error().stack!.split("\n").slice(2);
92-
if (
93-
stackLines &&
94-
stackLines.length > 0 &&
95-
stackLines[0].includes("ErrImpl")
96-
) {
92+
if (stackLines.length > 0 && stackLines[0]?.includes("ErrImpl")) {
9793
stackLines.shift();
9894
}
9995

@@ -281,4 +277,28 @@ export namespace Result {
281277
): val is Result<T, E> {
282278
return val instanceof Err || val instanceof Ok;
283279
}
280+
281+
/**
282+
* Takes an array (or tuple) of Results and returns an `Ok` containing all
283+
* success values in the same shape, or the first `Err` encountered.
284+
*/
285+
export function all<T extends readonly Result<unknown, unknown>[]>(
286+
results: [...T],
287+
): Result<
288+
{ [K in keyof T]: T[K] extends Result<infer U, any> ? U : never },
289+
{ [K in keyof T]: T[K] extends Result<any, infer F> ? F : never }[number]
290+
>;
291+
export function all<T, E>(results: Result<T, E>[]): Result<T[], E>;
292+
export function all(
293+
results: Result<unknown, unknown>[],
294+
): Result<unknown[], unknown> {
295+
const values: unknown[] = [];
296+
for (const result of results) {
297+
if (result.err) {
298+
return result;
299+
}
300+
values.push(result.val);
301+
}
302+
return new Ok(values);
303+
}
284304
}

0 commit comments

Comments
 (0)