Skip to content

Commit cca901c

Browse files
committed
docs: update changelog and documentation for smart type inference in Either, Maybe, and Result utilities
1 parent d77c733 commit cca901c

File tree

7 files changed

+184
-99
lines changed

7 files changed

+184
-99
lines changed

docs/changelog.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
---
88

9+
## [Unreleased]
10+
11+
### Added
12+
- **Smart type inference for combinators**: `all`, `sequence`, and `partition` now return `T[]` for homogeneous arrays and preserve tuple types for mixed types.
13+
- Before: `all([ok(1), ok(2), ok(3)])` returned `Result<[number, number, number], ...>` (tuple)
14+
- After: `all([ok(1), ok(2), ok(3)])` returns `Result<number[], ...>` (array)
15+
- Heterogeneous arrays still preserve tuple types: `all([ok(42), ok("hi")])``Result<[number, string], ...>`
16+
- Applies to `all`, `sequence` (Result/Either), and `partition` (Result/Either)
17+
- Makes `unwrapOr([])` work naturally without type errors
18+
19+
### Changed
20+
- **Improved `unwrapOr` flexibility**: Now accepts default values of different (but compatible) types.
21+
- Example: `all([ok(1), ok(2)]).unwrapOr([])` now works without type errors.
22+
- Before: Required exact tuple type like `[0, 0]`.
23+
- After: Accepts any compatible type like `[]` for arrays.
24+
25+
---
26+
927
## [1.2.0](https://github.com/richecr/holo-fn/releases/tag/v1.2.0) - 2025-12-08
1028

1129
### Added

docs/either/index.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -524,11 +524,13 @@ console.log(result); // true
524524

525525
Combines an array of `Either` values into a single `Either`. Returns `Right` with all values if all are `Right`, or `Left` with all errors if any are `Left`.
526526

527+
**Smart Type Inference**: For homogeneous arrays (same type), returns `Either<L[], R[]>`. For mixed types, preserves tuple types.
528+
527529
```ts
528530
import { all, left, right, type Either } from 'holo-fn/either';
529531

530-
const result1: Either<unknown, number[]> = all([right(1), right(2), right(3)]);
531-
console.log(result1.unwrapOr([])); // [1, 2, 3]
532+
const result1 = all([right(1), right(2), right(3)]);
533+
console.log(result1.unwrapOr([]));
532534

533535
const result2 = all([left('Name required'), left('Email invalid'), right(25)]);
534536
console.log(
@@ -538,9 +540,10 @@ console.log(
538540
})
539541
); // ['Name required', 'Email invalid']
540542

541-
// Empty array
542-
const result3 = all([]);
543-
console.log(result3.unwrapOr([])); // []
543+
const result3 = all([right(42), right("hello"), right(true)]);
544+
545+
const result4 = all([]);
546+
console.log(result4.unwrapOr([]));
544547
```
545548

546549
---
@@ -551,11 +554,13 @@ Combines an array of `Either` values into a single `Either`, stopping at the fir
551554

552555
Unlike `all` which collects all errors, `sequence` returns immediately when it finds the first `Left`.
553556

557+
**Smart Type Inference**: For homogeneous arrays (same type), returns `Either<L, R[]>`. For mixed types, preserves tuple types.
558+
554559
```ts
555560
import { left, right, sequence, type Either } from 'holo-fn/either';
556561

557-
const result1: Either<unknown, number[]> = sequence([right(1), right(2), right(3)]);
558-
console.log(result1.unwrapOr([])); // [1, 2, 3]
562+
const result1 = sequence([right(1), right(2), right(3)]);
563+
console.log(result1.unwrapOr([]));
559564

560565
const result2 = sequence([
561566
right(1),
@@ -565,7 +570,9 @@ const result2 = sequence([
565570
console.log(result2.match({
566571
left: (e) => e,
567572
right: (v) => v
568-
})); // 'First error' (not an array!)
573+
}));
574+
575+
const result3 = sequence([right(42), right("hello")]);
569576
```
570577

571578
---
@@ -576,6 +583,8 @@ Separates an array of `Either` values into two groups: `lefts` and `rights`. Alw
576583

577584
Unlike `all` and `sequence` which return an `Either`, `partition` returns a plain object with two arrays.
578585

586+
**Smart Type Inference**: For homogeneous arrays (same type), returns `{ lefts: L[], rights: R[] }`. For mixed types, preserves tuple types.
587+
579588
```ts
580589
import { left, partition, right } from 'holo-fn/either';
581590

@@ -588,8 +597,8 @@ const eithers = [
588597
];
589598

590599
const { lefts, rights } = partition(eithers);
591-
console.log(rights); // [1, 2, 3]
592-
console.log(lefts); // ['error1', 'error2']
600+
console.log(rights);
601+
console.log(lefts);
593602

594603
const { lefts: errors, rights: values } = partition(eithers);
595604
console.log(`✓ ${values.length} succeeded`);

docs/maybe/index.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -320,20 +320,21 @@ console.log(result); // true
320320

321321
Combines an array of `Maybe` values into a single `Maybe` containing an array. Returns `Just` with all values if all are `Just`, or `Nothing` if any is `Nothing`.
322322

323+
**Smart Type Inference**: For homogeneous arrays (same type), returns `Maybe<T[]>`. For mixed types, preserves tuple types like `Maybe<[number, string, boolean]>`.
324+
323325
```ts
324326
import { all, just, nothing, type Maybe } from 'holo-fn/maybe';
325327

326-
// All success case
327-
const result1: Maybe<number[]> = all([just(1), just(2), just(3)]);
328-
console.log(result1.unwrapOr([])); // [1, 2, 3]
328+
const result1 = all([just(1), just(2), just(3)]);
329+
console.log(result1.unwrapOr([]));
329330

330-
// Any failure case
331331
const result2 = all([just(1), nothing(), just(3)]);
332-
console.log(result2.isNothing()); // true
332+
console.log(result2.isNothing());
333+
334+
const result3 = all([just(42), just("hello"), just(true)]);
333335

334-
// Empty array
335-
const result3 = all([]);
336-
console.log(result3.unwrapOr([])); // []
336+
const result4 = all([]);
337+
console.log(result4.unwrapOr([]));
337338
```
338339

339340
---

docs/result/index.md

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -425,23 +425,27 @@ console.log(result2); // false
425425

426426
Combines an array of `Result` values into a single `Result`. Returns `Ok` with all values if all are `Ok`, or `Err` with all errors if any are `Err`.
427427

428+
**Smart Type Inference**: For homogeneous arrays (same type), returns `T[]`. For mixed types, preserves tuple types like `[number, string, boolean]`.
429+
428430
```ts
429431
import type { Result } from 'holo-fn';
430432
import { all, err, ok } from 'holo-fn/result';
431433

432-
const result1: Result<number[], unknown[]> = all([ok(1), ok(2), ok(3)]);
433-
console.log(result1.unwrapOr([])); // [1, 2, 3]
434+
const result1 = all([ok(1), ok(2), ok(3)]);
435+
console.log(result1.unwrapOr([]));
434436

435437
const result2 = all([err('Name required'), err('Email invalid'), ok(25)]);
436438
console.log(
437439
result2.match({
438440
ok: (v) => v,
439441
err: (e) => e,
440442
})
441-
); // ['Name required', 'Email invalid']
443+
);
444+
445+
const result3 = all([ok(42), ok("hello"), ok(true)]);
442446

443-
const result3 = all([]);
444-
console.log(result3.unwrapOr([])); // []
447+
const result4 = all([]);
448+
console.log(result4.unwrapOr([]));
445449
```
446450

447451
---
@@ -452,12 +456,14 @@ Combines an array of `Result` values into a single `Result`, stopping at the fir
452456

453457
Unlike `all` which collects all errors, `sequence` returns immediately when it finds the first `Err`.
454458

459+
**Smart Type Inference**: For homogeneous arrays (same type), returns `T[]`. For mixed types, preserves tuple types.
460+
455461
```ts
456462
import type { Result } from 'holo-fn';
457463
import { err, ok, sequence } from 'holo-fn/result';
458464

459-
const result1: Result<number[], unknown> = sequence([ok(1), ok(2), ok(3)]);
460-
console.log(result1.unwrapOr([])); // [1, 2, 3]
465+
const result1 = sequence([ok(1), ok(2), ok(3)]);
466+
console.log(result1.unwrapOr([]));
461467

462468
const result2 = sequence([
463469
ok(1),
@@ -467,7 +473,9 @@ const result2 = sequence([
467473
console.log(result2.match({
468474
ok: (v) => v,
469475
err: (e) => e
470-
})); // 'First error'
476+
}));
477+
478+
const result3 = sequence([ok(42), ok("hello")]);
471479
```
472480

473481
---
@@ -478,6 +486,8 @@ Separates an array of `Result` values into two groups: successes (`oks`) and fai
478486

479487
Unlike `all` and `sequence` which return a `Result`, `partition` returns a plain object with two arrays.
480488

489+
**Smart Type Inference**: For homogeneous arrays (same type), returns `{ oks: T[], errs: E[] }`. For mixed types, preserves tuple types.
490+
481491
```ts
482492
import { err, ok, partition } from 'holo-fn/result';
483493

@@ -490,8 +500,8 @@ const results = [
490500
];
491501

492502
const { oks, errs } = partition(results);
493-
console.log(oks); // [1, 2, 3]
494-
console.log(errs); // ['error1', 'error2']
503+
console.log(oks);
504+
console.log(errs);
495505

496506
const { oks: succeeded, errs: failed } = partition(results);
497507
console.log(`✓ ${succeeded.length} succeeded`);

src/either/index.ts

Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -428,99 +428,119 @@ type UnwrapLeftArray<T extends Either<unknown, unknown>[]> = {
428428
[K in keyof T]: T[K] extends Either<infer L, unknown> ? L : never;
429429
}[number];
430430

431+
type ExtractEitherRight<T> = T extends Either<unknown, infer R> ? R : never;
432+
type ExtractEitherLeft<T> = T extends Either<infer L, unknown> ? L : never;
433+
431434
/**
432435
* Combines multiple Either values, collecting all Left values.
433-
* Supports heterogeneous tuple types.
436+
* For homogeneous arrays, returns R[] instead of tuples. For mixed types, preserves tuple types.
434437
*
435438
* @example
436439
* ```ts
437-
* all([right(1), right(2), right(3)]); // Right([1, 2, 3])
440+
* all([right(1), right(2), right(3)]); // Right<number[]>
438441
* all([right(1), left("e1"), left("e2")]); // Left(["e1", "e2"])
442+
* all([right(42), right("hello")]); // Right<[number, string]>
439443
* ```
440444
*/
445+
export function all<
446+
T extends Either<L, R>,
447+
L = ExtractEitherLeft<T>,
448+
R = ExtractEitherRight<T>,
449+
>(eithers: T[]): Either<L[], R[]>;
441450
export function all<T extends Either<unknown, unknown>[]>(
442451
eithers: [...T],
443-
): Either<UnwrapLeftArray<T>[], UnwrapEitherArray<T>> {
444-
const values: UnwrapEitherArray<T>[number][] = [];
445-
const errors: UnwrapLeftArray<T>[] = [];
452+
): Either<UnwrapLeftArray<T>[], UnwrapEitherArray<T>>;
453+
export function all<T extends Either<unknown, unknown>[]>(
454+
eithers: T,
455+
): Either<unknown, unknown> {
456+
const values: unknown[] = [];
457+
const errors: unknown[] = [];
446458

447459
for (const either of eithers) {
448460
if (either.isLeft()) {
449-
errors.push(either.extract() as UnwrapLeftArray<T>);
461+
errors.push(either.extract());
450462
} else {
451-
values.push(either.extract() as UnwrapEitherArray<T>[number]);
463+
values.push(either.extract());
452464
}
453465
}
454466

455467
if (errors.length > 0) {
456-
return new Left(errors) as Either<
457-
UnwrapLeftArray<T>[],
458-
UnwrapEitherArray<T>
459-
>;
468+
return new Left(errors);
460469
}
461470

462-
return new Right(values) as Either<
463-
UnwrapLeftArray<T>[],
464-
UnwrapEitherArray<T>
465-
>;
471+
return new Right(values);
466472
}
467473

468474
/**
469475
* Combines multiple Either values with fail-fast behavior.
470-
* Supports heterogeneous tuple types.
476+
* For homogeneous arrays, returns R[] instead of tuples. For mixed types, preserves tuple types.
471477
*
472478
* @example
473479
* ```ts
474-
* sequence([right(1), right(2), right(3)]); // Right([1, 2, 3])
480+
* sequence([right(1), right(2), right(3)]); // Right<number[]>
475481
* sequence([right(1), left("e1"), left("e2")]); // Left("e1") - stops at first
482+
* sequence([right(42), right("hello")]); // Right<[number, string]>
476483
* ```
477484
*/
485+
export function sequence<
486+
T extends Either<L, R>,
487+
L = ExtractEitherLeft<T>,
488+
R = ExtractEitherRight<T>,
489+
>(eithers: T[]): Either<L, R[]>;
478490
export function sequence<T extends Either<unknown, unknown>[]>(
479491
eithers: [...T],
480-
): Either<UnwrapLeftArray<T>, UnwrapEitherArray<T>> {
481-
const values: UnwrapEitherArray<T>[number][] = [];
492+
): Either<UnwrapLeftArray<T>, UnwrapEitherArray<T>>;
493+
export function sequence<T extends Either<unknown, unknown>[]>(
494+
eithers: T,
495+
): Either<unknown, unknown> {
496+
const values: unknown[] = [];
482497

483498
for (const either of eithers) {
484499
if (either.isLeft()) {
485-
return new Left(either.extract() as UnwrapLeftArray<T>) as Either<
486-
UnwrapLeftArray<T>,
487-
UnwrapEitherArray<T>
488-
>;
500+
return new Left(either.extract());
489501
}
490502

491-
values.push(either.extract() as UnwrapEitherArray<T>[number]);
503+
values.push(either.extract());
492504
}
493505

494-
return new Right(values) as Either<UnwrapLeftArray<T>, UnwrapEitherArray<T>>;
506+
return new Right(values);
495507
}
496508

497509
/**
498510
* Separates an array of Eithers into Left and Right values.
499-
* Supports heterogeneous tuple types.
511+
* For homogeneous arrays, returns R[] instead of tuples. For mixed types, preserves tuple types.
500512
*
501513
* @example
502514
* ```ts
503515
* partition([right(1), left("e1"), right(2)]);
504-
* // { lefts: ["e1"], rights: [1, 2] }
516+
* // { lefts: string[], rights: number[] } (not tuples!)
505517
* ```
506518
*/
519+
export function partition<
520+
T extends Either<L, R>,
521+
L = ExtractEitherLeft<T>,
522+
R = ExtractEitherRight<T>,
523+
>(eithers: T[]): { lefts: L[]; rights: R[] };
507524
export function partition<T extends Either<unknown, unknown>[]>(
508525
eithers: [...T],
509-
): { lefts: UnwrapLeftArray<T>[]; rights: UnwrapEitherArray<T> } {
526+
): { lefts: UnwrapLeftArray<T>[]; rights: UnwrapEitherArray<T> };
527+
export function partition<T extends Either<unknown, unknown>[]>(
528+
eithers: T,
529+
): { lefts: unknown[]; rights: unknown[] } {
510530
return eithers.reduce(
511531
(acc, either) => {
512532
if (either.isLeft()) {
513-
acc.lefts.push(either.extract() as UnwrapLeftArray<T>);
533+
acc.lefts.push(either.extract());
514534
} else {
515-
acc.rights.push(either.extract() as UnwrapEitherArray<T>[number]);
535+
acc.rights.push(either.extract());
516536
}
517537
return acc;
518538
},
519539
{
520-
lefts: [] as UnwrapLeftArray<T>[],
521-
rights: [] as UnwrapEitherArray<T>[number][],
540+
lefts: [] as unknown[],
541+
rights: [] as unknown[],
522542
},
523-
) as { lefts: UnwrapLeftArray<T>[]; rights: UnwrapEitherArray<T> };
543+
);
524544
}
525545

526546
/**

0 commit comments

Comments
 (0)