Skip to content

Commit 00bcc71

Browse files
feat: improve zipWidth element count (#210)
1 parent 2e0d3bc commit 00bcc71

File tree

3 files changed

+184
-16
lines changed

3 files changed

+184
-16
lines changed

src/maybe/maybe.interface.ts

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -534,25 +534,80 @@ export interface IMaybe<T> extends IMonad<T> {
534534
flatMapMany<R>(fn: (val: NonNullable<T>) => Promise<R>[]): Promise<IMaybe<NonNullable<R>[]>>
535535

536536
/**
537-
* Combines this Maybe with another Maybe using a combiner function.
538-
*
539-
* If both Maybes are Some, applies the function to their values and returns
540-
* a new Some containing the result. If either is None, returns None.
541-
*
537+
* Combines this Maybe with one or more other Maybes using a combiner function.
538+
*
539+
* If all Maybes are Some, applies the function to their values and returns
540+
* a new Some containing the result. If any is None, returns None.
541+
*
542542
* @typeParam U - The type of the value in the other Maybe
543543
* @typeParam R - The type of the combined result
544544
* @param other - Another Maybe to combine with this one
545545
* @param fn - A function that combines the values from both Maybes
546-
* @returns A new Maybe containing the combined result if both inputs are Some, otherwise None
547-
*
546+
* @returns A new Maybe containing the combined result if all inputs are Some, otherwise None
547+
*
548548
* @example
549-
* // Combine user name and email into a display string
549+
* // Combine two values
550550
* const name = maybe(user.name);
551551
* const email = maybe(user.email);
552-
*
552+
*
553553
* const display = name.zipWith(email, (name, email) => `${name} <${email}>`);
554-
* // Some("John Doe <[email protected]>") if both name and email exist
554+
* // Some("John Doe <[email protected]>") if both exist
555555
* // None if either is missing
556+
*
557+
* @example
558+
* // Combine three values
559+
* const firstName = maybe(user.firstName);
560+
* const lastName = maybe(user.lastName);
561+
* const email = maybe(user.email);
562+
*
563+
* const contact = firstName.zipWith(lastName, email, (first, last, email) => ({
564+
* fullName: `${first} ${last}`,
565+
* email
566+
* }));
567+
* // Some({ fullName: "John Doe", email: "john@example.com" }) if all exist
568+
* // None if any is missing
569+
*
570+
* @example
571+
* // Combine many values
572+
* const result = a.zipWith(b, c, d, e, (a, b, c, d, e) => a + b + c + d + e);
556573
*/
557-
zipWith<U extends NonNullable<unknown>, R>(other: IMaybe<U>, fn: (a: NonNullable<T>, b: U) => NonNullable<R>): IMaybe<R>
574+
zipWith<U extends NonNullable<unknown>, R>(
575+
other: IMaybe<U>,
576+
fn: (a: NonNullable<T>, b: U) => NonNullable<R>
577+
): IMaybe<R>
578+
579+
zipWith<U extends NonNullable<unknown>, V extends NonNullable<unknown>, R>(
580+
m1: IMaybe<U>,
581+
m2: IMaybe<V>,
582+
fn: (a: NonNullable<T>, b: U, c: V) => NonNullable<R>
583+
): IMaybe<R>
584+
585+
zipWith<U extends NonNullable<unknown>, V extends NonNullable<unknown>, W extends NonNullable<unknown>, R>(
586+
m1: IMaybe<U>,
587+
m2: IMaybe<V>,
588+
m3: IMaybe<W>,
589+
fn: (a: NonNullable<T>, b: U, c: V, d: W) => NonNullable<R>
590+
): IMaybe<R>
591+
592+
zipWith<U extends NonNullable<unknown>, V extends NonNullable<unknown>, W extends NonNullable<unknown>, X extends NonNullable<unknown>, R>(
593+
m1: IMaybe<U>,
594+
m2: IMaybe<V>,
595+
m3: IMaybe<W>,
596+
m4: IMaybe<X>,
597+
fn: (a: NonNullable<T>, b: U, c: V, d: W, e: X) => NonNullable<R>
598+
): IMaybe<R>
599+
600+
zipWith<U extends NonNullable<unknown>, V extends NonNullable<unknown>, W extends NonNullable<unknown>, X extends NonNullable<unknown>, Z extends NonNullable<unknown>, R>(
601+
m1: IMaybe<U>,
602+
m2: IMaybe<V>,
603+
m3: IMaybe<W>,
604+
m4: IMaybe<X>,
605+
m5: IMaybe<Z>,
606+
fn: (a: NonNullable<T>, b: U, c: V, d: W, e: X, f: Z) => NonNullable<R>
607+
): IMaybe<R>
608+
609+
// Variadic overload for 5+ Maybes
610+
zipWith<R>(
611+
...args: [...IMaybe<NonNullable<unknown>>[], (...values: NonNullable<unknown>[]) => NonNullable<R>]
612+
): IMaybe<R>
558613
}

src/maybe/maybe.spec.ts

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -858,13 +858,110 @@ describe('Maybe', () => {
858858
it('should return None if both values are None', () => {
859859
const first = maybe<string>()
860860
const second = maybe<string>()
861-
861+
862862
const result = first.zipWith(second, (a, b) => `${a}, ${b}!`)
863-
863+
864+
expect(result.isNone()).toBe(true)
865+
})
866+
867+
it('should combine three Some values', () => {
868+
const a = maybe(1)
869+
const b = maybe(2)
870+
const c = maybe(3)
871+
872+
const result = a.zipWith(b, c, (x, y, z) => x + y + z)
873+
874+
expect(result.isSome()).toBe(true)
875+
expect(result.valueOr(0)).toBe(6)
876+
})
877+
878+
it('should return None if any of three values is None', () => {
879+
const a = maybe(1)
880+
const b = maybe<number>()
881+
const c = maybe(3)
882+
883+
const result = a.zipWith(b, c, (x, y, z) => x + y + z)
884+
885+
expect(result.isNone()).toBe(true)
886+
})
887+
888+
it('should combine four Some values', () => {
889+
const a = maybe('a')
890+
const b = maybe('b')
891+
const c = maybe('c')
892+
const d = maybe('d')
893+
894+
const result = a.zipWith(b, c, d, (w, x, y, z) => w + x + y + z)
895+
896+
expect(result.isSome()).toBe(true)
897+
expect(result.valueOr('')).toBe('abcd')
898+
})
899+
900+
it('should return None if any of four values is None', () => {
901+
const a = maybe('a')
902+
const b = maybe('b')
903+
const c = maybe<string>()
904+
const d = maybe('d')
905+
906+
const result = a.zipWith(b, c, d, (w, x, y, z) => w + x + y + z)
907+
864908
expect(result.isNone()).toBe(true)
865909
})
910+
911+
it('should combine five Some values', () => {
912+
const a = maybe(1)
913+
const b = maybe(2)
914+
const c = maybe(3)
915+
const d = maybe(4)
916+
const e = maybe(5)
917+
918+
const result = a.zipWith(b, c, d, e, (v1, v2, v3, v4, v5) => v1 + v2 + v3 + v4 + v5)
919+
920+
expect(result.isSome()).toBe(true)
921+
expect(result.valueOr(0)).toBe(15)
922+
})
923+
924+
it('should return None if any of five values is None', () => {
925+
const a = maybe(1)
926+
const b = maybe(2)
927+
const c = maybe(3)
928+
const d = maybe(4)
929+
const e = maybe<number>()
930+
931+
const result = a.zipWith(b, c, d, e, (v1, v2, v3, v4, v5) => v1 + v2 + v3 + v4 + v5)
932+
933+
expect(result.isNone()).toBe(true)
934+
})
935+
936+
it('should return None if any of six values is None', () => {
937+
const a = maybe(1)
938+
const b = maybe(2)
939+
const c = maybe(3)
940+
const d = maybe(4)
941+
const e = maybe(5)
942+
const f = maybe<number>()
943+
944+
const result = a.zipWith(b, c, d, e, f, (v1, v2, v3, v4, v5, v6) => v1 + v2 + v3 + v4 + v5 + v6)
945+
946+
expect(result.isNone()).toBe(true)
947+
})
948+
949+
it('should work with mixed types', () => {
950+
const name = maybe('John')
951+
const age = maybe(30)
952+
const active = maybe(true)
953+
954+
const result = name.zipWith(age, active, (n, a, act) => ({ name: n, age: a, active: act }))
955+
956+
expect(result.isSome()).toBe(true)
957+
expect(result.valueOr({ name: '', age: 0, active: false })).toEqual({
958+
name: 'John',
959+
age: 30,
960+
active: true
961+
})
962+
})
866963
})
867-
964+
868965
describe('flatMapMany', () => {
869966
it('should execute multiple promises in parallel when Some', async () => {
870967
const source = maybe(42)

src/maybe/maybe.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,23 @@ export class Maybe<T> implements IMaybe<T> {
301301
.catch(() => new Maybe<NonNullable<R>[]>())
302302
}
303303

304-
public zipWith<U extends NonNullable<unknown>, R>(other: IMaybe<U>, fn: (a: NonNullable<T>, b: U) => NonNullable<R>): IMaybe<R> {
305-
return this.flatMap(a => other.map(b => fn(a, b)))
304+
public zipWith<R>(...args: unknown[]): IMaybe<R> {
305+
if (this.isNone()) {
306+
return new Maybe<R>()
307+
}
308+
309+
const fn = args[args.length - 1] as (...values: unknown[]) => NonNullable<R>
310+
const maybes = args.slice(0, -1) as IMaybe<unknown>[]
311+
312+
const values: unknown[] = [this.value]
313+
314+
for (const m of maybes) {
315+
if (m.isNone()) {
316+
return new Maybe<R>()
317+
}
318+
values.push(m.valueOrThrow())
319+
}
320+
321+
return new Maybe<R>(fn(...values))
306322
}
307323
}

0 commit comments

Comments
 (0)