Skip to content

Instantly share code, notes, and snippets.

@cesalberca
Created January 23, 2020 18:14
Show Gist options
  • Save cesalberca/b3292c7cc9b70af46c4e4541bb8da3bf to your computer and use it in GitHub Desktop.
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/
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())
})
})
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
}
}
@bliheris
Copy link

Thank you very much for providing very useful code.

@bfunc
Copy link

bfunc commented Jan 15, 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment