Created
January 23, 2020 18:14
-
-
Save cesalberca/b3292c7cc9b70af46c4e4541bb8da3bf to your computer and use it in GitHub Desktop.
Maybe monad implemented in TypeScript. Base of the work of https://codewithstyle.info/advanced-functional-programming-in-typescript-maybe-monad/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Maybe } from './maybe' | |
describe('Maybe', () => { | |
it('should handle a value', () => { | |
const maybe = Maybe.some('test') | |
expect(maybe.getOrElse('')).toBe('test') | |
}) | |
it('should handle an undefined value', () => { | |
const maybe = Maybe.fromValue<string>(undefined) | |
expect(maybe.getOrElse('test')).toBe('test') | |
}) | |
it('should a false value', () => { | |
const maybe = Maybe.fromValue<boolean>(false) | |
expect(maybe.getOrElse(true)).toBe(false) | |
}) | |
it('should handle a string value', () => { | |
const maybe = Maybe.fromValue('test') | |
expect(maybe.getOrElse('')).toBe('test') | |
}) | |
it('should handle an empty string value', () => { | |
const maybe = Maybe.fromValue('') | |
expect(maybe.getOrElse('test')).toBe('') | |
}) | |
it('should return a default value with a different type', () => { | |
const maybe = Maybe.fromValue<string>(null) | |
expect(maybe.getOrElse(null)).toBe(null) | |
}) | |
it('should handle a null value', () => { | |
const maybe = Maybe.fromValue<string>(null) | |
expect(maybe.getOrElse('test')).toBe('test') | |
}) | |
it('should handle a numeric value', () => { | |
const maybe = Maybe.fromValue(42) | |
expect(maybe.getOrElse(0)).toBe(42) | |
}) | |
it('should handle the zero value as valid', () => { | |
const maybe = Maybe.fromValue(0) | |
expect(maybe.getOrElse(1)).toBe(0) | |
}) | |
it('should tap a value', () => { | |
const maybe = Maybe.fromValue('value') | |
let actual = false | |
maybe.tap(() => { | |
actual = true | |
}) | |
expect(actual).toBe(true) | |
}) | |
it('should not tap a value', () => { | |
const maybe = Maybe.none() | |
let actual = false | |
maybe.tap(() => { | |
actual = true | |
}) | |
expect(actual).toBe(false) | |
}) | |
it('should throw an error if the value is not valid', () => { | |
expect(() => { | |
Maybe.some(null) | |
}).toThrowError() | |
}) | |
it('should handle a callback as a default value', () => { | |
const mock = jest.fn() | |
const maybe = Maybe.fromValue(null) | |
maybe.getOrExecute(mock) | |
expect(mock).toHaveBeenCalled() | |
}) | |
it('should check if it has a value', () => { | |
const maybe = Maybe.fromValue('hello') | |
expect(maybe.has()).toBe(true) | |
}) | |
it('should check if it does not have a value', () => { | |
const maybe = Maybe.fromValue<string>(null) | |
expect(maybe.has()).toBe(false) | |
}) | |
it('should handle none value', () => { | |
const maybe = Maybe.none() | |
expect(maybe.getOrElse('test')).toBe('test') | |
}) | |
it('should get or throw', () => { | |
const maybe = Maybe.none() | |
expect(() => { | |
maybe.getOrThrow(new Error('foo')) | |
}).toThrowError('foo') | |
}) | |
it('should be able to map existing values', () => { | |
const maybeMap = Maybe.some({ a: 'a' }) | |
expect(maybeMap.map(e => e.a).getOrElse('b')).toBe('a') | |
}) | |
it('should be able to map non existing values', () => { | |
type Type = { foo: Maybe<{ bar: string }> } | |
const maybeMap = Maybe.some<Type>({ foo: Maybe.none() }) | |
expect( | |
maybeMap | |
.getOrExecute(() => { | |
throw new Error() | |
}) | |
.foo.map(x => x.bar) | |
).toEqual(Maybe.none()) | |
}) | |
it('should be able to flat map existing values', () => { | |
type Type = { foo: Maybe<{ bar: string }> } | |
const maybeMap = Maybe.fromValue<Type>({ foo: Maybe.some({ bar: 'qux' }) }) | |
expect(maybeMap.flatMap(x => x.foo).map(x => x.bar)).toEqual(Maybe.some('qux')) | |
}) | |
it('should be able to flat map non existing values', () => { | |
type Type = { foo: Maybe<{ bar: string }> } | |
const maybeMap = Maybe.none<Type>() | |
expect(maybeMap.flatMap(x => x.foo)).toEqual(Maybe.none()) | |
}) | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type CallbackFunction<T = unknown> = (...params: unknown[]) => T | |
export class Maybe<T> { | |
private constructor(private value: T | null) {} | |
static some<T>(value: T): Maybe<T> { | |
if (!this.isValid(value)) { | |
throw new Error('Provided value must not be empty') | |
} | |
return new Maybe(value) | |
} | |
static none<T>(): Maybe<T> { | |
return new Maybe<T>(null) | |
} | |
static fromValue<T>(value: T | undefined | null): Maybe<T> { | |
return this.isValid(value) ? Maybe.some(value as T) : Maybe.none<T>() | |
} | |
private static isValid(value: unknown | null | undefined): boolean { | |
return !!value || this.isNumberZero(value) || this.isFalse(value) || this.isEmptyString(value) | |
} | |
private static isNumberZero<R>(value: R): boolean { | |
return typeof value === 'number' && value === 0 | |
} | |
private static isEmptyString<R>(value: R): boolean { | |
return typeof value === 'string' && value === '' | |
} | |
private static isFalse<R>(value: R): boolean { | |
return typeof value === 'boolean' && !value | |
} | |
has(): boolean { | |
return this.value !== null | |
} | |
getOrElse<R = T>(defaultValue: T | R): T | R { | |
return this.value === null ? defaultValue : this.value | |
} | |
getOrExecute(defaultValue: CallbackFunction<T>): T { | |
return this.value === null ? defaultValue() : this.value | |
} | |
map<R>(f: (wrapped: T) => R): Maybe<R> { | |
if (this.value === null) { | |
return Maybe.none<R>() | |
} else { | |
return Maybe.some(f(this.value)) | |
} | |
} | |
tap(f: (wrapped: T) => void): Maybe<T> { | |
if (this.value !== null) { | |
f(this.value) | |
} | |
return Maybe.fromValue(this.value) | |
} | |
flatMap<R>(f: (wrapped: T) => Maybe<R>): Maybe<R> { | |
if (this.value === null) { | |
return Maybe.none<R>() | |
} else { | |
return f(this.value) | |
} | |
} | |
getOrThrow(error?: Error): T { | |
return this.value === null | |
? (() => { | |
if (error !== undefined) { | |
throw error | |
} | |
throw new Error() | |
})() | |
: this.value | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you very much for providing very useful code.