Skip to content

Commit dbda528

Browse files
committed
feat: update rambda dependency to version 10.3.4 and enhance type safety in Either, Maybe, and Result utilities
1 parent eeb4d37 commit dbda528

File tree

10 files changed

+309
-62
lines changed

10 files changed

+309
-62
lines changed

bun.lock

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

docs/either/index.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,3 +599,97 @@ errors.forEach((err) => console.error(err));
599599

600600
---
601601

602+
## Common Patterns
603+
604+
### Discriminated union errors
605+
606+
```ts
607+
import { pipe } from 'rambda';
608+
import { left, match, type Either } from 'holo-fn/either';
609+
610+
type User = {
611+
name: string;
612+
email: string;
613+
};
614+
615+
type ValidationError =
616+
| { type: 'INVALID_EMAIL'; email: string }
617+
| { type: 'TOO_SHORT'; minLength: number }
618+
| { type: 'REQUIRED'; field: string };
619+
620+
const result: Either<ValidationError, User> = left({
621+
type: 'INVALID_EMAIL',
622+
email: 'bad@email',
623+
});
624+
625+
const message = pipe(
626+
result,
627+
match({
628+
left: (err) => {
629+
switch (err.type) {
630+
case 'INVALID_EMAIL':
631+
return `Invalid email: ${err.email}`;
632+
case 'TOO_SHORT':
633+
return `Must be at least ${err.minLength} characters`;
634+
case 'REQUIRED':
635+
return `Field ${err.field} is required`;
636+
}
637+
},
638+
right: (user) => `Success: ${user.name}`,
639+
})
640+
);
641+
```
642+
643+
### Batch operations with error tracking
644+
645+
```ts
646+
import { pipe } from 'rambda';
647+
import { all, left, match, right } from 'holo-fn/either';
648+
649+
type AddressData = {
650+
street: string;
651+
city: string;
652+
zip: string;
653+
};
654+
655+
type UserData = {
656+
email: string;
657+
age: number;
658+
address: AddressData;
659+
};
660+
661+
const validateEmail = (email: string) => (email.includes('@') ? right(email) : left('Invalid email'));
662+
663+
const validateAge = (age: number) => (age >= 18 ? right(age) : left('Must be 18+'));
664+
665+
const validateAddress = (address: AddressData) => {
666+
if (!address.street || !address.city || !address.zip) {
667+
return left('Incomplete address');
668+
}
669+
return right(address);
670+
};
671+
672+
const validateForm = (data: UserData) =>
673+
pipe(
674+
all([validateEmail(data.email), validateAge(data.age), validateAddress(data.address)]),
675+
match({
676+
left: (err) => `Validation failed: ${err}`,
677+
right: ([email, age, address]) =>
678+
`Validation succeeded: ${email}, ${age}, ${address.street}, ${address.city}, ${address.zip}`,
679+
})
680+
);
681+
682+
console.log(
683+
validateForm({
684+
685+
age: 25,
686+
address: {
687+
street: '123 Main St',
688+
city: 'Anytown',
689+
zip: '12345',
690+
},
691+
})
692+
);
693+
```
694+
695+
---

docs/maybe/index.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,3 +337,72 @@ console.log(result3.unwrapOr([])); // []
337337

338338
---
339339

340+
## Common Patterns
341+
342+
### Combining multiple Maybes
343+
344+
When you need to work with multiple `Maybe` values:
345+
346+
```ts
347+
import { pipe } from 'rambda';
348+
import { all, just, match } from 'holo-fn/maybe';
349+
350+
const name = just('Test User');
351+
const age = just(25);
352+
const email = just('[email protected]');
353+
354+
const user = pipe(
355+
all([name, age, email]),
356+
match({
357+
just: ([n, a, e]) => ({ name: n, age: a, email: e }),
358+
nothing: () => null,
359+
})
360+
);
361+
362+
console.log(user);
363+
// Output: { name: 'Test User', age: 25, email: '[email protected]' }
364+
365+
```
366+
367+
### Conditional logic with predicates
368+
369+
```ts
370+
import { pipe } from 'rambda';
371+
import { just, match } from 'holo-fn/maybe';
372+
373+
const value = just(42);
374+
375+
const category = pipe(
376+
value,
377+
match({
378+
just: (v) => {
379+
if (v > 100) return 'big';
380+
if (v > 50) return 'medium';
381+
return 'small';
382+
},
383+
nothing: () => 'unknown',
384+
})
385+
);
386+
387+
console.log(`The number is categorized as: ${category}`);
388+
```
389+
390+
### Chaining operations with early exit
391+
392+
```ts
393+
import { pipe } from 'rambda';
394+
import { chain, filter, fromNullable, map } from 'holo-fn/maybe';
395+
396+
const user: { email?: string } | null = { email: '[email protected]' };
397+
398+
const result = pipe(
399+
fromNullable(user),
400+
chain((u) => fromNullable(u.email)),
401+
filter((email) => email.includes('@')),
402+
map((email) => email.toLowerCase())
403+
);
404+
405+
console.log(result); // Just('[email protected]')
406+
```
407+
408+
---

docs/result/index.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,3 +495,59 @@ failed.forEach((err) => console.error(err));
495495

496496
---
497497

498+
## Common Patterns
499+
500+
### Error handling with specific error types
501+
502+
```ts
503+
import { pipe } from 'rambda';
504+
import { err, match, type Result } from 'holo-fn/result';
505+
506+
type ApiError = 'NOT_FOUND' | 'UNAUTHORIZED' | 'SERVER_ERROR';
507+
508+
type User = {
509+
id: number;
510+
name: string;
511+
};
512+
513+
const result: Result<User, ApiError> = err('NOT_FOUND');
514+
515+
const message = pipe(
516+
result,
517+
match({
518+
ok: (user) => `Welcome ${user.name}`,
519+
err: (e) => {
520+
switch (e) {
521+
case 'NOT_FOUND':
522+
return 'Resource not found';
523+
case 'UNAUTHORIZED':
524+
return 'Access denied';
525+
case 'SERVER_ERROR':
526+
return 'Server error occurred';
527+
}
528+
},
529+
})
530+
);
531+
532+
console.log(message);
533+
```
534+
535+
### Validation pipelines
536+
537+
```ts
538+
import { pipe } from 'rambda';
539+
import { map, ok, validate } from 'holo-fn/result';
540+
541+
const validateAge = (age: number) =>
542+
pipe(
543+
ok<number, string>(age),
544+
validate((n) => n >= 0, 'Age must be positive'),
545+
validate((n) => n <= 150, 'Age must be realistic'),
546+
validate((n) => n >= 18, 'Must be 18 or older'),
547+
map((n) => ({ age: n, isAdult: true }))
548+
);
549+
550+
console.log(validateAge(25));
551+
```
552+
553+
---

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"eslint": "^9.27.0",
4646
"eslint-plugin-prettier": "^5.4.0",
4747
"eslint-plugin-security": "^3.0.1",
48-
"rambda": "^10.2.0",
48+
"rambda": "^10.3.4",
4949
"typescript-eslint": "^8.32.1"
5050
},
5151
"peerDependencies": {

src/either/index.ts

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -169,52 +169,66 @@ export const equals =
169169
return either.equals(other);
170170
};
171171

172-
export const all = <L, R>(eithers: Either<L, R>[]): Either<L[], R[]> => {
173-
const values: R[] = [];
174-
const errors: L[] = [];
172+
type UnwrapEitherArray<T extends Either<unknown, unknown>[]> = {
173+
[K in keyof T]: T[K] extends Either<unknown, infer R> ? R : never;
174+
};
175+
176+
type UnwrapLeftArray<T extends Either<unknown, unknown>[]> = {
177+
[K in keyof T]: T[K] extends Either<infer L, unknown> ? L : never;
178+
}[number];
179+
180+
export function all<T extends Either<unknown, unknown>[]>(
181+
eithers: [...T]
182+
): Either<UnwrapLeftArray<T>[], UnwrapEitherArray<T>> {
183+
const values: UnwrapEitherArray<T>[number][] = [];
184+
const errors: UnwrapLeftArray<T>[] = [];
175185

176186
for (const either of eithers) {
177187
if (either.isLeft()) {
178-
errors.push(either.extract() as L);
188+
errors.push(either.extract() as UnwrapLeftArray<T>);
179189
} else {
180-
values.push(either.extract() as R);
190+
values.push(either.extract() as UnwrapEitherArray<T>[number]);
181191
}
182192
}
183193

184194
if (errors.length > 0) {
185-
return new Left<L[], R[]>(errors);
195+
return new Left(errors) as Either<UnwrapLeftArray<T>[], UnwrapEitherArray<T>>;
186196
}
187197

188-
return new Right<L[], R[]>(values);
189-
};
198+
return new Right(values) as Either<UnwrapLeftArray<T>[], UnwrapEitherArray<T>>;
199+
}
190200

191-
export const sequence = <L, R>(eithers: Either<L, R>[]): Either<L, R[]> => {
192-
const values: R[] = [];
201+
export function sequence<T extends Either<unknown, unknown>[]>(
202+
eithers: [...T]
203+
): Either<UnwrapLeftArray<T>, UnwrapEitherArray<T>> {
204+
const values: UnwrapEitherArray<T>[number][] = [];
193205

194206
for (const either of eithers) {
195207
if (either.isLeft()) {
196-
return new Left<L, R[]>(either.extract() as L);
208+
return new Left(either.extract() as UnwrapLeftArray<T>) as Either<UnwrapLeftArray<T>, UnwrapEitherArray<T>>;
197209
}
198210

199-
values.push(either.extract() as R);
211+
values.push(either.extract() as UnwrapEitherArray<T>[number]);
200212
}
201213

202-
return new Right<L, R[]>(values);
203-
};
214+
return new Right(values) as Either<UnwrapLeftArray<T>, UnwrapEitherArray<T>>;
215+
}
204216

205-
export const partition = <L, R>(eithers: Either<L, R>[]): { lefts: L[]; rights: R[] } => {
217+
export function partition<T extends Either<unknown, unknown>[]>(
218+
eithers: [...T]
219+
): { lefts: UnwrapLeftArray<T>[]; rights: UnwrapEitherArray<T> } {
206220
return eithers.reduce(
207221
(acc, either) => {
208222
if (either.isLeft()) {
209-
acc.lefts.push(either.extract() as L);
223+
acc.lefts.push(either.extract() as UnwrapLeftArray<T>);
210224
} else {
211-
acc.rights.push(either.extract() as R);
225+
acc.rights.push(either.extract() as UnwrapEitherArray<T>[number]);
212226
}
213227
return acc;
214228
},
215-
{ lefts: [] as L[], rights: [] as R[] }
216-
);
217-
};
229+
{ lefts: [] as UnwrapLeftArray<T>[], rights: [] as UnwrapEitherArray<T>[number][] }
230+
) as { lefts: UnwrapLeftArray<T>[]; rights: UnwrapEitherArray<T> };
231+
}
218232

219233
export const left = <L, R = never>(value: L): Either<L, R> => new Left(value);
220234
export const right = <L, R>(value: R): Either<L, R> => new Right(value);

src/maybe/index.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,19 +132,23 @@ export const equals =
132132
return maybe.equals(other);
133133
};
134134

135-
export const all = <T>(maybes: Maybe<T>[]): Maybe<T[]> => {
136-
const values: T[] = [];
135+
type UnwrapMaybeArray<T extends Maybe<unknown>[]> = {
136+
[K in keyof T]: T[K] extends Maybe<infer V> ? V : never;
137+
};
138+
139+
export function all<T extends Maybe<unknown>[]>(maybes: [...T]): Maybe<UnwrapMaybeArray<T>> {
140+
const values: UnwrapMaybeArray<T>[number][] = [];
137141

138142
for (const maybe of maybes) {
139143
if (maybe.isNothing()) {
140-
return new Nothing<T[]>();
144+
return new Nothing() as Maybe<UnwrapMaybeArray<T>>;
141145
}
142146

143-
values.push(maybe.extract());
147+
values.push(maybe.extract() as UnwrapMaybeArray<T>[number]);
144148
}
145149

146-
return new Just(values);
147-
};
150+
return new Just(values) as Maybe<UnwrapMaybeArray<T>>;
151+
}
148152

149153
export const just = <T>(value: T): Maybe<T> => new Just(value);
150154
export const nothing = <T = never>() => new Nothing<T>();

0 commit comments

Comments
 (0)