diff --git a/package.json b/package.json index 34791d9..1e33343 100644 --- a/package.json +++ b/package.json @@ -68,5 +68,8 @@ }, "release": { "branch": "master" + }, + "dependencies": { + "simplytyped": "^1.0.5" } } diff --git a/src/index.ts b/src/index.ts index 9344749..645c0e0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,5 +13,6 @@ Maybe[fl.of] = maybe; export { some } from './some'; export { none } from './none'; +export { maybeT, MaybeT } from './transformer'; export default Maybe; diff --git a/src/transformer.ts b/src/transformer.ts new file mode 100644 index 0000000..b08e0dd --- /dev/null +++ b/src/transformer.ts @@ -0,0 +1,66 @@ +import { ConstructorFor, Unknown, Nullable } from 'simplytyped'; +// @ts-ignore +import Maybe, { MatchType, Nil } from './maybe'; +import { maybe } from './index'; + +export interface Monad { + map: (f: (v: T) => Nullable) => any; +} + +export type MonadLike = Monad | PromiseLike; +export type MonadValue> = + T extends PromiseLike ? U : + T extends Monad ? U : never; + +export type MaybeValue> = NonNullable>; + +const isPromise = (x: any): x is PromiseLike => typeof x.then === 'function'; + +const getMap = (x: MonadLike): Monad['map'] => { + if (isPromise(x)) return x.then.bind(x) as any; + + return x.map.bind(x) as any; +}; + +export class MaybeT> { + private constructor(private value: T) {} + + static maybeT>(monad: V) { + return new MaybeT(monad); + } + + map(f: (v: MaybeValue) => U): MaybeT> { + const map = getMap(this.value); + return new MaybeT(map(inner => + maybe(inner) + .map(f) + .asNullable() as any, + )); + } + + caseOf(matcher: MatchType, R>): MaybeT> { + const map = getMap(this.value); + return new MaybeT(map(inner => + maybe(inner) + .caseOf(matcher) + .asNullable() as any, + )); + } + + orElse>(def: U | (() => U)): U { + const map = getMap(this.value); + return map(inner => + maybe(inner) + .orElse(def), + ); + } + + asNullable() { return this.value; } + asType>>>(c: ConstructorFor): M { + if (!(this.value instanceof c)) throw new Error(`Expected value to be instance of monad ${c.name}`); + + return this.value; + } +} + +export const maybeT = MaybeT.maybeT; diff --git a/tests/transformer.test.ts b/tests/transformer.test.ts new file mode 100644 index 0000000..245d93b --- /dev/null +++ b/tests/transformer.test.ts @@ -0,0 +1,102 @@ +import { maybeT } from '../src'; + +test('array - can generate a maybeT', () => { + const raw = [1, 2, null, undefined, 5]; + const x = maybeT(raw); + const got = x.asNullable(); + + expect(got).toEqual(raw); +}); + +test('array - can map function over non-nil elements in array', () => { + const x = maybeT([1, null, 1, undefined, 1]); + + const y = x.map(v => { + expect(v).toBe(1); + return 2; + }).asNullable(); + + expect(y).toEqual([2, null, 2, null, 2]); +}); + +test('array - can get back array type from maybeT', () => { + const x = maybeT(['1', '2', null, '4']); + + const y = x + .map(v => parseInt(v)) + .map(v => v + 1) + .asType>(Array); + + expect(y).toEqual([2, 3, null, 5]); +}); + +test('array - cannot get back wrong monadic type', () => { + const x = maybeT(['hi']); + + expect(() => { + x.asType>(Promise); + }).toThrowError(); +}); + +test('array - can pattern match over values', () => { + const x = maybeT(['1', '2', null, '4']); + + const y = x + .caseOf({ + none: () => 3, + some: v => parseInt(v), + }) + .asNullable(); + + expect(y).toEqual([1, 2, 3, 4]); +}); + +test('array - can default null values', () => { + const x = maybeT([1, 2, null, 4]); + + const y = x.orElse(3); + + expect(y).toEqual([1, 2, 3, 4]); +}); + +test('promise - can generate a maybeT', async () => { + const value = 'hi'; + const x = maybeT(Promise.resolve(value)); + + const got = await x.asNullable(); + + expect(got).toBe(value); +}); + +test('promise - cannot create maybe with rejected promise', async () => { + expect.assertions(1); + const value = 'hey'; + const x = maybeT(Promise.reject(value)); + + try { + await x.asNullable(); + } catch (e) { + expect(e).toBe(value); + } +}); + +test('promise - can map function over non-nil value', async () => { + const x = maybeT(Promise.resolve('hey')); + + const got = await x + .map(v => v + ' there') + .asType>(Promise); + + expect(got).toBe('hey there'); +}); + +test('promise - will not map function over nil values', async () => { + const x = maybeT(Promise.resolve(null)); + + const got = await x + .map(v => { + throw new Error("I shouldn't be here!"); + }).asNullable(); + + expect(got).toBe(null); +});