Skip to content

Commit

Permalink
feat: Add bunch of helper functions
Browse files Browse the repository at this point in the history
- `compact` - removes None values, unwraps the rest
- `all` - `Some` if all values in array are `Some`, `None` otherwise
- `allProperties` - `Some` if all values in array are some/defined,
`None` othrwise.
- `first` - returns first not-None item from the array or None if all of
the items are None.
  • Loading branch information
SevInf committed May 5, 2019
1 parent f28af81 commit 4691499
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 18 deletions.
28 changes: 28 additions & 0 deletions src/__tests__/all.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { maybe } from '../maybe';
import { all } from '../all';

describe('all', () => {
it('returns empty array if given empty array', () => {
expect(all([]).orThrow()).toEqual([]);
});

it('returns values if all of them are not-non', () => {
expect(
all([maybe('foo'), maybe('bar'), maybe('baz')]).orThrow()
).toEqual(['foo', 'bar', 'baz']);
});

it('work with heterogenous arrays', () => {
expect(all([maybe('foo'), maybe(5), maybe(false)]).orThrow()).toEqual([
'foo',
5,
false
]);
});

it('return none if any of the items is none', () => {
expect(all([maybe('foo'), maybe(null), maybe('bar')]).isNone()).toBe(
true
);
});
});
62 changes: 62 additions & 0 deletions src/__tests__/allProperties.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { maybe } from '../maybe';
import { allProperties } from '../allProperties';

describe('allProperties', () => {
it('return object if all of the properties are not None', () => {
expect(
allProperties({
foo: maybe('foo'),
bar: maybe(42),
baz: maybe(false)
}).orThrow()
).toEqual({
foo: 'foo',
bar: 42,
baz: false
});
});

it('allows to mix in non-maybe proeprties', () => {
expect(
allProperties({
foo: 'foo',
bar: maybe(42),
baz: maybe(false)
}).orThrow()
).toEqual({
foo: 'foo',
bar: 42,
baz: false
});
});

it('return none if at least one property is None', () => {
expect(
allProperties({
foo: maybe('foo'),
bar: maybe(null),
baz: maybe(false)
}).isNone()
).toBe(true);
});

it('return none if at least one property is null', () => {
expect(
allProperties({
foo: maybe('foo'),
bar: null,
baz: maybe(false)
}).isNone()
).toBe(true);
});

it('return none if at least one property is undefined', () => {
expect(
allProperties({
foo: maybe('foo'),
bar: undefined,
baz: maybe(false)
}).isNone()
).toBe(true);
});
});
20 changes: 20 additions & 0 deletions src/__tests__/compact.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { maybe } from '../maybe';
import { compact } from '../compact';

describe('compact', () => {
it('returns empty array if given empty array', () => {
expect(compact([])).toEqual([]);
});

it('filters out None values', () => {
expect(
compact([
maybe('foo'),
maybe(null),
maybe('bar'),
maybe(undefined),
maybe('baz')
])
).toEqual(['foo', 'bar', 'baz']);
});
});
19 changes: 19 additions & 0 deletions src/__tests__/first.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { maybe } from '../maybe';
import { first } from '../first';

describe('first', () => {
it('returns none for empty array', () => {
expect(first([]).isNone()).toBe(true);
});

it('returns none if all items are none', () => {
expect(
first([maybe(null), maybe(undefined), maybe(null)]).isNone()
).toBe(true);
});
it('returns first non-none item', () => {
expect(first([maybe(null), maybe('foo'), maybe('bar')]).orThrow()).toBe(
'foo'
);
});
});
79 changes: 79 additions & 0 deletions src/all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Maybe, none, some } from './maybe';

function all<T1>(maybies: [Maybe<T1>]): Maybe<[T1]>;
function all<T1, T2>(maybies: [Maybe<T1>, Maybe<T2>]): Maybe<[T1, T2]>;
function all<T1, T2, T3>(
maybies: [Maybe<T1>, Maybe<T2>, Maybe<T3>]
): Maybe<[T1, T2, T3]>;
function all<T1, T2, T3, T4>(
maybies: [Maybe<T1>, Maybe<T2>, Maybe<T3>, Maybe<T4>]
): Maybe<[T1, T2, T3, T4]>;
function all<T1, T2, T3, T4, T5>(
maybies: [Maybe<T1>, Maybe<T2>, Maybe<T3>, Maybe<T4>, Maybe<T5>]
): Maybe<[T1, T2, T3, T4, T5]>;
function all<T1, T2, T3, T4, T5, T6>(
maybies: [Maybe<T1>, Maybe<T2>, Maybe<T3>, Maybe<T4>, Maybe<T5>, Maybe<T6>]
): Maybe<[T1, T2, T3, T4, T5, T6]>;
function all<T1, T2, T3, T4, T5, T6, T7>(
maybies: [
Maybe<T1>,
Maybe<T2>,
Maybe<T3>,
Maybe<T4>,
Maybe<T5>,
Maybe<T6>,
Maybe<T7>
]
): Maybe<[T1, T2, T3, T4, T5, T6, T7]>;
function all<T1, T2, T3, T4, T5, T6, T7, T8>(
maybies: [
Maybe<T1>,
Maybe<T2>,
Maybe<T3>,
Maybe<T4>,
Maybe<T5>,
Maybe<T6>,
Maybe<T7>,
Maybe<T8>
]
): Maybe<[T1, T2, T3, T4, T5, T6, T7, T8]>;
function all<T1, T2, T3, T4, T5, T6, T7, T8, T9>(
maybies: [
Maybe<T1>,
Maybe<T2>,
Maybe<T3>,
Maybe<T4>,
Maybe<T5>,
Maybe<T6>,
Maybe<T7>,
Maybe<T8>,
Maybe<T9>
]
): Maybe<[T1, T2, T3, T4, T5, T6, T7, T8, T9]>;
function all<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(
maybies: [
Maybe<T1>,
Maybe<T2>,
Maybe<T3>,
Maybe<T4>,
Maybe<T5>,
Maybe<T6>,
Maybe<T7>,
Maybe<T8>,
Maybe<T9>,
Maybe<T10>
]
): Maybe<[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]>;
function all<T>(maybies: Array<Maybe<T>>): Maybe<T[]>;
function all(maybies: Array<Maybe<unknown>>): Maybe<unknown[]> {
const result: unknown[] = [];
for (const item of maybies) {
if (item.isNone()) {
return none;
}
result.push(item.orThrow());
}
return some(result);
}

export { all };
21 changes: 21 additions & 0 deletions src/allProperties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Maybe, RemoveMaybe, Defined, none, maybe, some } from './maybe';

type UnwrapMaybeProperties<T extends {}> = {
[K in keyof T]: Defined<RemoveMaybe<T[K]>>
};

export function allProperties<T extends {}>(
object: T
): Maybe<UnwrapMaybeProperties<T>> {
const result = {} as UnwrapMaybeProperties<T>;
const keys = Object.keys(object) as (keyof T)[];

for (const key of keys) {
const value = maybe(object[key]);
if (value.isNone()) {
return none;
}
result[key] = value.orThrow();
}
return some(result);
}
11 changes: 11 additions & 0 deletions src/compact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Maybe } from './maybe';
export function compact<T>(items: Array<Maybe<T>>): T[] {
const result = [] as T[];
for (const item of items) {
const unpacked = item.orNull();
if (unpacked !== null) {
result.push(unpacked);
}
}
return result;
}
9 changes: 9 additions & 0 deletions src/first.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Maybe, none } from './maybe';
export function first<T>(variants: Array<Maybe<T>>): Maybe<T> {
for (const variant of variants) {
if (!variant.isNone()) {
return variant;
}
}
return none;
}
43 changes: 25 additions & 18 deletions src/maybe.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
type Required<T> = Exclude<T, null | undefined>;
type RemoveMaybe<T> = T extends Maybe<infer Inner> ? Inner : T;
export type Defined<T> = Exclude<T, null | undefined>;
export type RemoveMaybe<T> = T extends Maybe<infer Inner> ? Inner : T;
type MaybeOnce<T> = Maybe<RemoveMaybe<T>>;
type MapCallback<T, U> = (arg: Required<T>) => U;
type MapCallback<T, U> = (arg: Defined<T>) => U;

export interface Maybe<T> {
isNone(): boolean;

orElse(fallback: Required<T>): Required<T>;
orCall(getFallback: () => Required<T>): Required<T>;
orNull(): Required<T> | null;
orThrow(message?: string): Required<T>;
orElse(fallback: Defined<T>): Defined<T>;
orCall(getFallback: () => Defined<T>): Defined<T>;
orNull(): Defined<T> | null;
orThrow(message?: string): Defined<T>;

map<U>(f: MapCallback<T, U>): MaybeOnce<U>;
get<K extends keyof Required<T>>(key: K): MaybeOnce<Required<T>[K]>;
get<K extends keyof Defined<T>>(key: K): MaybeOnce<Defined<T>[K]>;
}

const none: Maybe<any> = {
export const none: Maybe<any> = {
isNone() {
return true;
},

orElse(fallback: Required<any>): Required<any> {
orElse(fallback: Defined<any>): Defined<any> {
return fallback;
},

orCall(getFallback: () => Required<any>): Required<any> {
orCall(getFallback: () => Defined<any>): Defined<any> {
return getFallback();
},

Expand All @@ -46,33 +46,33 @@ const none: Maybe<any> = {
};

class Some<T> implements Maybe<T> {
constructor(private readonly value: Required<T>) {}
constructor(private readonly value: Defined<T>) {}

isNone(): boolean {
return false;
}

orElse(): Required<T> {
orElse(): Defined<T> {
return this.value;
}

orCall(): Required<T> {
orCall(): Defined<T> {
return this.value;
}

orNull(): Required<T> | null {
orNull(): Defined<T> | null {
return this.value;
}

orThrow(): Required<T> {
orThrow(): Defined<T> {
return this.value;
}

map<U>(f: MapCallback<T, U>): MaybeOnce<U> {
return maybe(f(this.value));
}

get<K extends keyof Required<T>>(key: K): MaybeOnce<Required<T>[K]> {
get<K extends keyof Defined<T>>(key: K): MaybeOnce<Defined<T>[K]> {
return this.map(obj => obj[key]);
}
}
Expand All @@ -89,5 +89,12 @@ export function maybe<T>(value: T | null | undefined): MaybeOnce<T> {
return none;
}

return (new Some(value as Required<T>) as Maybe<T>) as MaybeOnce<T>;
return some(value) as MaybeOnce<T>;
}

export function some<T>(value: T): Maybe<T> {
if (value == null) {
throw new Error('Some expects non-null values');
}
return new Some(value as Defined<T>);
}

0 comments on commit 4691499

Please sign in to comment.