README
=> Fat Arrow ·
Fat Arrow is a library for Typed Functional Programming in TypeScript compatible with Node.js and all major browsers.
:warning: Alpha release! API may change :warning:
Installation
npm install fat-arrow-ts
Setup Jest matchers
Be sure to have a reference to a setup file in your jest.config.ts
// In jest.config.ts
export default {
setupFilesAfterEnv: [
'./setupTests.ts'
],
}
Include this in your setup file
// In setupTests.ts
import 'fat-arrow-ts/jest-matchers'
Quick start
import { left, right, Either } from 'fat-arrow-ts';
const getDivision = (numerator: number, denominator: number): Either<Error, number> => {
if (denominator === 0) {
return left(new Error('Division by zero!'))
}
return right(numerator / denominator)
}
const addTwo = (number: number) => number + 2
const print = (value: Either<Error, number>) =>
value.fold(
(error) => console.error('Doh!', error.message),
(result) => console.log(`Result is ${result}. Hooray!`)
)
print(getDivision(10, 0).flatMap(addTwo)) // Doh! Division by zero!
print(getDivision(10, 5).flatMap(addTwo)) // Result is 4. Hooray!
Essentials
Flattening
Fat Arrow's factory functions and data types' methods support flattening to accept both TS native values and data types themselves; in the latter case, data types objects will be flattened.
import { right } from 'fat-arrow-ts';
const myValue = right<Error, number>(5)
console.log(right(myValue).equals(myValue)) // true
Maybe
An Maybe<A>
value is useful to model nullable values.
maybe
Takes a value in input and creates a Maybe<A>
object.
- if the input value is non-nullable the produced object will have just state.
- if the input value is nullable (
null | undefined
) the produced object will have none state;
import { Maybe, maybe } from 'fat-arrow-ts';
const myMap = new Map([
['key1', 'value1'],
['key2', 'value2'],
])
const getValue = (key: string): Maybe<string> => maybe(myMap.get(key))
//-- If value is "just" --//
const existing = getValue('key1')
console.log(existing.fold()) // 'value1'
console.log(existing.isJust) // true
// Flattening
console.log(maybe(existing).equals(existing)) // true
console.log(maybe(existing).isJust) // true
//-- If value is "none" --//
const missing = getValue('foo')
console.log(missing.fold()) // undefined
console.log(missing.isNone) // true
// Flattening
console.log(maybe(missing).equals(missing)) // true
console.log(maybe(missing).isNone) // true
just
Takes a non-nullable value in input and creates a Maybe<A>
object with just state.
import { just, none, maybe } from 'fat-arrow-ts';
const myValue = just(5)
console.log(myValue.fold()) // 5
console.log(myValue.isJust) // true
// Flattening
console.log(maybe(myValue).equals(myValue)) // true
console.log(just(myValue).equals(myValue)) // true
none
Creates a Maybe<A>
object with none state.
import { just, none, maybe } from 'fat-arrow-ts';
const myValue = none()
console.log(myValue.fold()) // null
console.log(myValue.isLeft) // true
console.log(myValue === none()) // true
// Flattening
console.log(maybe(myValue).equals(myValue)) // true
console.log(just(myValue).equals(myValue)) // true
Maybe API
isJust
States if Maybe<A>
is in just state.
import { just } from 'fat-arrow-ts';
const myValue = just(5)
console.log(myValue.isJust) // true
isNone
States if Maybe<A>
is in none state.
import { none } from 'fat-arrow-ts';
const myValue = none()
console.log(myValue.isNone) // true
equals
Takes an Maybe<any, any>
in input and asserts if the passed value has the same state and structural equality.
import { just, none } from 'fat-arrow-ts';
// With just()
const aJustValue = just({ foo: 'foo' })
console.log(aJustValue.equals(aJustValue)) // true
const anotherJustValue = just({ bar: 'bar' })
console.log(anotherJustValue.equals(aJustValue)) // false
// Deep comparison
console.log(aJustValue.equals(just({ foo: 'foo' }))) // true
// With none()
const aNoneValue = none()
console.log(aNoneValue.equals(none())) // true
console.log(aNoneValue === none()) // true
fold
It lets you handle or unwrap the raw value in your data type instances.
It comes with two overloaded call signatures
() => void | A
: will return the value as it is(ifNone: () => B, isJust: (just: A) => B) => B
: will accept two callbacks that will let you trigger side effects or map the value before returning it.
import { right, left } from 'fat-arrow-ts';
const aJustValue = just(5)
console.log(myValue.fold()) // 5
// Mapping values
const aNoneValue = none()
console.log(aNoneValue.fold(() => 0, it => it)) // 0
// Triggering side effects
aNoneValue.fold(
() => {
// Only the 'none' callback will be applied to the value
console.error('Nothing to see here!') // 'Nothing to see here!'
},
it => {
console.log(it)
}
)
flatMap
Takes a callback of type <B>(value: A) => B | Maybe<B>
and applies it to the just value of your type class instances.
By default flatMap
method will try to convert the returned value to an Maybe<B>
just state so that you can also produce raw values from your callbacks.
Returning a none value, you can switch to a none state.
If you are used to ES Promises you may find similarities with the .then()
method.
import { just, none, Maybe } from 'fat-arrow-ts';
const myValue = just(5)
const justResult: Maybe<number> = myValue.flatMap(
it => it + 5
)
console.log(justResult.isJust) // true
console.log(justResult.fold()) // 10
// Will be flattened
const sameTypeJustResult: Maybe<number> = myValue.flatMap(
it => just(it + 5)
)
console.log(sameTypeJustResult.isJust) // true
console.log(sameTypeJustResult.fold()) // 10
// You can return just values with a different type
const anotherTypeJustResult: Maybe<string> = myValue.flatMap(
it => just('foo')
)
console.log(anotherTypeJustResult.isJust) // true
console.log(anotherTypeJustResult.fold()) // 'foo'
// You can return none values
const noneResult: Maybe<number> = myValue.flatMap(
it => none()
)
console.log(noneResult.isNone) // true
console.log(noneResult.fold()) // undefined
mapIf
Works very similar to flatMap
but it also accepts a predicate (value: A) => boolean
as first parameter.
It will map your type class instances only if the predicate returns true
.
import { Maybe } from 'fat-arrow-ts';
const getUserPersonalWebsiteFromInput = (): Maybe<string> => {
//...
}
const maybeInput = getUserPersonalWebsiteFromInput()
// Only a non-nullable input will be trimmed
const normalizedUrl = maybeInput.mapIf(
it => it.endsWith('/'), it => it.slice(0, -1)
)
orElse
It takes a callback of type () => A | Maybe<A>
and lets you recover from a nullable value.
If you are used to ES Promises you may find similarities with the .catch()
method.
import { Maybe } from 'fat-arrow-ts';
const getUserInput = (): Maybe<string> => {
//...
}
const maybeInput = getUserInput()
const recovered = maybeInput.orElse(() => 'N/A')
console.log(recovered.isJust) // true
console.log(recovered.fold()) // 'N/A'
toEither
TBD...
Maybe Jest matchers
See Setup Jest matchers for installation instructions.
toBeJust
Asserts if expected
is just and has the expected value. It accepts both raw values and data type instances.
import { just } from 'fat-arrow-ts'
it('is just', () => {
const actual = maybe(5) // or just(5)
expect(actual).toBeJust(5);
})
toBeNone
Asserts if expected
is none.
import { none } from 'fat-arrow-ts'
it('is none', () => {
const actual = none()
expect(actual).toBeNone();
})
toHaveBeenLastCalledWithJust
Asserts if a jest.Mock
has been called last time with the expected just value
it('is called with just', () => {
const spy = jest.fn()
runYourCode(spy)
expect(spy).toHaveBeenLastCalledWithJust('expected just value');
})
toHaveBeenLastCalledWithNone
Asserts if a jest.Mock
has been called last time with a none value
it('is called with none', () => {
const spy = jest.fn()
runYourCode(spy)
expect(spy).toHaveBeenLastCalledWithNone();
})
Either
An Either<E, A>
value may contain either a value of type E
or a value of type A
, at any given time. In other words, it could be in left state or right state.
right
Takes a value in input and creates an Either<E, A>
object with right state.
import { right } from 'fat-arrow-ts';
const myValue = right<Error, number>(5)
console.log(myValue.fold()) // 5
// Flattening
console.log(left(myValue).isRight) // true
console.log(right(myValue).equals(myValue)) // true
left
Takes a value in input and creates an Either<E, A>
object with left state.
import { left } from 'fat-arrow-ts';
const myValue = left<Error, number>(new Error('Ouch!'))
console.log(myValue.fold()) // Error
// Flattening
console.log(left(myValue).equals(myValue)) // true
console.log(right(myValue).isLeft) // true
Either API
Let's go through all Either<E, A>
properties and methods
isRight
States if Either<E, A>
is in right state.
import { right } from 'fat-arrow-ts';
const myValue = right<Error, number>(5)
console.log(myValue.isRight) // true
isLeft
States if Either<E, A>
is in left state.
import { left } from 'fat-arrow-ts';
const myValue = left<Error, number>(new Error('Ouch!'))
console.log(myValue.isLeft) // true
equals
Takes an Either<any, any>
in input and asserts if the passed value has the same state and structural equality.
import { right, left } from 'fat-arrow-ts';
const aRightValue = right<object, object>({ foo: 'foo' })
console.log(aRightValue.equals(aRightValue)) // true
const anotherRightValue = right<object, object>({ bar: 'bar' })
console.log(anotherRightValue.equals(aRightValue)) // false
const aLeftValueWithSameContents = left<object, object>({ foo: 'foo' })
console.log(aLeftValueWithSameContents.equals(aRightValue)) // false
// Deep comparison
console.log(aRightValue.equals(right({ foo: 'foo' }))) // true
fold
It lets you handle or unwrap the raw value in your data type instances.
It comes with two overloaded call signatures
() => E | A
: will return the value as it is(ifLeft: (left: E) => B, ifRight: (right: A) => B) => B
: will accept two callbacks that will let you trigger side effects or map the value before returning it.
import { right, left } from 'fat-arrow-ts';
const aRightValue = right<Error, number>(5)
console.log(myValue.fold()) // 5
// Mapping values
const aLeftValue = left<Error, number>(new Error('Ouch!'))
console.log(aLeftValue.fold(e => 0, it => it)) // 0
// Triggering side effects
aLeftValue.fold(
e => {
// Only the left callback will be applied to the value
console.error(e) // Error
},
it => {
console.log(it)
}
)
flatMap
Takes a callback of type (value: A) => B | Either<E, B>
and applies it to the right value of your type class instances.
By default flatMap
method will try to convert the returned value to an Either<E, B>
right state so that you can also produce raw values from your callback.
Returning a left value, you can switch to a left state.
If you are used to ES Promises you may find similarities with the .then()
method.
import { right, left } from 'fat-arrow-ts';
const myValue = right<Error, number>(5)
const rightResult: Either<Error, number> = myValue.flatMap(
it => it + 5
)
console.log(rightResult.isRight) // true
console.log(rightResult.fold()) // 5
// Will be flattened
const sameTypeResult: Either<Error, number> = myValue.flatMap(
it => right(it + 5)
)
console.log(sameTypeResult.isRight) // true
console.log(sameTypeResult.fold()) // 5
// You can return right values with a different type
const anotherTypeRightResult: Either<Error, string> = myValue.flatMap(
it => right('foo')
)
console.log(anotherTypeRightResult.isRight) // true
console.log(anotherTypeRightResult.fold()) // 'foo'
// Will be flattened to a Either<Error, number> with left state
const leftResult: Either<Error, number> = myValue.flatMap(
it => left<Error, number>(new Error())
)
console.log(leftResult.isLeft) // true
console.log(leftResult.fold()) // Error
mapIf
Works very similar to flatMap
but it also accepts a predicate (value: A) => boolean
as first parameter.
It will map your type class instances only if the predicate returns true
.
import { right, left } from 'fat-arrow-ts';
const isFizzBuzz = (it: number) => it % 15 === 0
const isFizz = (it: number) => it % 3 === 0
const isBuzz = (it: number) => it % 5 === 0
const fizzBuzz = (i: number) =>
right<string, number>(i)
.mapIf(isFizzBuzz, () => left('FizzBuzz'))
.mapIf(isFizz, () => left('Fizz'))
.mapIf(isBuzz, () => left('Buzz'))
.fold()
console.log(fizzBuzz(3)) // Fizz
console.log(fizzBuzz(5)) // Buzz
console.log(fizzBuzz(15)) // FizzBuzz
console.log(fizzBuzz(2)) // 2
mapLeft
Similar to flatMap
, it will let you apply callbacks of type (value: E) => G | Either<G, A>
to the left value of your type class instances.
By default mapLeft
method will try to convert the returned value to an Either<G, A>
left state so that you can also produce raw values from your callback.
Returning a right value, you can switch your type class instances to a right state.
import { right, left } from 'fat-arrow-ts';
const myValue = left<Error, number>(new Error('Ouch!'))
const leftResult: Either<Error, number> = myValue.mapLeft(
it => new Error(`Error was ${it.message}`)
)
console.log(leftResult.isLeft) // true
console.log(leftResult.fold()) // Error
// Will be flattened
const sameTypeLeftResult: Either<Error, number> = myValue.mapLeft(
it => left(new Error(`Error was ${it.message}`))
)
console.log(sameTypeLeftResult.isLeft) // true
console.log(sameTypeLeftResult.fold()) // Error
// You can return left values with a different type
const anotherTypeLeftResult: Either<string, number> = myValue.mapLeft(
it => left('foo')
)
console.log(anotherTypeLeftResult.isLeft) // true
console.log(anotherTypeLeftResult.fold()) // 'foo'
// Will be flattened to a Either<Error, number> with right state
const rightResult: Either<Error, number> = myValue.mapLeft(
it => right(5)
)
console.log(rightResult.isRight) // true
console.log(rightResult.fold()) // 5
orElse
It takes a callback of type (value: E) => A | Either<E, A>
and applies it to the left value of your type class instances.
The main difference with mapLeft
is that it will try to convert the mapped value to a right state. A useful tool to recover from errors.
If you are used to ES Promises you may find similarities with the .catch()
method.
import { Either } from 'fat-arrow-ts';
const getUserInput = (): Either<Error, string> => {
//...
}
const userInput = getUserInput()
const recovered = userInput.orElse(e => {
console.error(e) // Error
return 'Who cares!'
})
console.log(recovered.isRight) // true
console.log(recovered.fold()) // 'Who cares!'
bimap
TBD...
toMaybe
TBD...
Either Jest matchers
See Setup Jest matchers for installation instructions.
toBeRight
Asserts if expected
is right and has the expected value. It accepts both raw values and data type instances.
import { right } from 'fat-arrow-ts'
it('is right', () => {
const actual = right<Error, number>(5)
expect(actual).toBeRight(5);
})
toBeLeft
Asserts if expected
is left and has the expected value. It accepts both raw values and data type instances.
import { left } from 'fat-arrow-ts'
it('is left', () => {
const actual = left<Error, number>(new Error())
expect(actual).toBeLeft(new Error());
})
toHaveBeenLastCalledWithRight
Asserts if a jest.Mock
has been called last time with the expected right value
it('is called with right', () => {
const spy = jest.fn()
runYourCode(spy)
expect(spy).toHaveBeenLastCalledWithRight('expected right value');
})
toHaveBeenLastCalledWithLeft
Asserts if a jest.Mock
has been called last time with the expected left value
it('is called with left', () => {
const spy = jest.fn()
runYourCode(spy)
expect(spy).toHaveBeenLastCalledWithLeft('expected left value');
})
Result
A Result<A>
is a type alias for Either<Error, A>
.
tryCatch
It takes a callback () => A | Result<A>
in input that will be run safely and returns a Result<A>
instance.
- if the callback runs correctly the result of the callback will be returned as a
Result<A>
with right state. - if the callback throws an error, the
Error
will be returned as aResult<A>
with left state.
import { tryCatch, Result } from 'fat-arrow-ts';
const getFullName = (name: string, surname: string): string => {
if (name.length < 1 || name.surname < 1) {
throw new Error()
}
return `${name} ${surname}`
}
//-- If callback runs correctly --//
const result: Result<string> = tryCatch(() => getFullName('John', 'Doe'))
const myValue = result.flatMap((it) => it.toUpperCase())
console.log(myValue.fold()) // JOHN DOE
console.log(myValue.isRight) // true
//-- If callback throws --//
const safeResult: Result<string> = tryCatch(() => getFullName('', ''))
const mySafeValue = safeResult.flatMap((it) => it.toUpperCase())
console.log(mySafeValue.fold()) // Error
console.log(mySafeValue.isLeft) // true
Validation
A Validation<E, A>
is a type alias for Either<E[], A>
.
As you can see you can have many left values for a single right value. This kind of data type is useful to model validation outputs.
validate
It takes a value in input and an array of validation functions. It then runs through all the validation functions collecting all possible left values, if any.
A validation function must return a pass
value, containing A
, otherwise a fail
value containing E
.
If all the validations pass the validation output will have right state, otherwise a left state.
import { fail, pass, validate, Validation } from 'fat-arrow-ts'
const isPasswordLongEnough = (password: string): Validation<string, string> =>
password.length > 6 ? pass(password) : fail('Password must have more than 6 characters.')
const isPasswordStrongEnough = (password: string): Validation<string, string> =>
/[\W]/.test(password) ? pass(password) : fail('Password must contain a special character.')
const validations = [isPasswordLongEnough, isPasswordStrongEnough]
const validatePassword = (pwd: string) => validate(pwd, validations)
//-- If all validations pass --//
console.log(validatePassword('qwertyu!').fold()) // 'qwertyu!'
console.log(validatePassword('qwertyu!').isRight) // true
//-- If one (or more) validations fail --//
console.log(validatePassword('qwerty').fold()) // ['Password must have more than 6 characters.', 'Password must contain a special character.']
console.log(validatePassword('qwerty').isLeft) // true
pass
Takes a value in input and creates a Validation<E, A>
object with right state.
import { pass, fail } from 'fat-arrow-ts';
const myValue = pass(5)
console.log(myValue.fold()) // 5
console.log(myValue.isRight) // true
// Flattening
console.log(pass(myValue).equals(myValue)) // true
console.log(fail(myValue).equals(myValue)) // true
fail
Takes a value in input and creates a Validation<E, A>
object with left state.
The input could be a plain E
value or an array E[]
.
import { pass, fail } from 'fat-arrow-ts';
const myValue = fail('error')
console.log(myValue.fold()) // ['error']
console.log(myValue.isLeft) // true
// Using arrays
const anotherValue = fail(['first error', 'second error'])
console.log(myValue.fold()) // ['first error', 'second error']
console.log(myValue.isLeft) // true
// Flattening
console.log(pass(myValue).equals(myValue)) // true
console.log(fail(myValue).equals(myValue)) // true