Seeking a Type Safe Sanctuary in JavaScript

Todd Brown
Nerd For Tech
Published in
4 min readApr 3, 2021

--

There is a growing community of strongly typed scripting languages that transpile into JavaScript, with Typescript leading the way. The temptation of catching type issues in a compile phase is sometimes strong enough that we reach of a more type safe tool. But we can get type safety (or certainty) through an alternative technique — guarding your functions. There are a few tutorials around the concept of guarding your functions, many years ago I found this blog by Mike Stay (which eventually dives into category theory).

Becoming familiar with the evolving JavaScript library ecosystem is an enormous task. Sanctuary is a library that was introduced in 2015 (still under active development) that combines many of the “Functional” helpers from Ramda, Fantasy Land compliant types, and type safety.

Before we do anything we need to include the various library bits:

const { cond, equals, T } = require('ramda');
const { pipe, Left, Right, EitherType } = require('sanctuary');
const $ = require('sanctuary-def');
const def = $.create({ checkTypes: true, env: $.env });

Type Guarding the Function Divide

The interesting thing in this snippet is the def function. When we use this to define a function, it curries the function and sets up type guards. The interesting bits are the third and fourth parameters. The third is an array of types, with the last item in the array being the return type of the function. The fourth is the implementation. We can leverage this to define divide:

const divideImpl =  (n, d) => cond([
[equals(0), (_) => { throw new Error("division by zero" }) ],
[T, (d) => n/d ]
])(d)
// divide = Number -> Number -> Number
const divide = def(
'divide',
{},
[$.Number, $.Number, $.Number],
divideImpl
)

Divide takes a numerator and a denominator, evaluates the denominator against zero where it will throw an exception, otherwise the function returns the quotient (shocking — right?).

const x = divide(10)(2) //=> 5
const x = divide(10)(0) //=> Error: divsion by zero

if we change the type signature of divide to return a string and invoke it, we can capture a detailed error message.

//    divide =  Number -> Number -> String
const divide = def(
'divide',
{},
[$.Number, $.Number, $.String],
divideImpl
)
divide(10)(2)


/*
TypeError: Invalid value

divide :: Number -> Number -> String
^^^^^^
1

1) 5 :: Number

The value at position 1 is not a member of ‘String’.

*/

In the above case the TypeError is telling us that the function is trying to return the numeric value 5, but the function signature is expecting a string. Let’s revert the return type back to Number and consider the slippery slope of throwing exceptions.

Dealing with the Exceptions

The main problem with exceptions is that they often can impose flow control and unwind issues if not handled delicately. Their introduction of a second way of exiting a function violates much of what we were taught in 101. In a revised approach, the binary type Either is ideally situated to capturing either the output or the exception from the method, allowing a single point of entry and exit from the function. Instead of coding it into the function itself, we could create a higher ordered function to invoke a funcion and capture exceptions on our behalf — something like

const a = $.TypeVariable('a');
const l = $.TypeVariable('l');
const r = $.TypeVariable('r');
const fromThrowableImpl = (f, a) => {
try{
return Right(f.call({}, a))
}
catch(l) {
return Left(l)
}
}
// fromThrowable :: ( a -> r) -> a -> Either l r
const fromThrowable = def(
'fromThrowable',
{},
[$.Function([a, r]), a, EitherType(l, r)],
fromThrowableImpl
);

the constants a, l, and r represent generic types that remain constant throughout type definition. fromThrowable attempts to invoke a function and capture any exceptions and returns Either.Right (the instance of Either typically used in Success, with the returned value encapsulated) or Either .Left (the instance of Either typically used in Failure with the error code encapsulated). fromThrowable itself takes two parameters. The first is a function that takes a type and returns a different type. The second is a variable that satisfies the input to that function.

fromThrowable(divide(10), 2) 
//=> Right(5)
fromThrowable(divide(10), 0)
//=> Left(new Error("Division by zero"))

Fortunately Sanctuary saves us the hastle o fhaving to define fromThrowable, by providing encaseEither. The caveat is that encaseEither takes an additional function to transform any errors.

encaseEither((x)=>x, divide(10), 0) 
//=> Left(new Error("division by zero"))

A better guard

This is all nice, and can be applied to any function that throws an exception. This is great choice when leveraging legacy JavaScript libraries. In this project we can protect more by using a better type definition, by introducing a new type to the Sanctuary environment,

const isIntegerNeqZero = x => $.test([], $.Integer, x) && x != 0const NonZeroInteger = $.NullaryType(
'my-test/NonZeroInteger',
'http://engieering.somewhere.com/my-test#NonZeroInteger',
isIntegerNeqZero
);

NonZeroInteger assures that any values are both integers and not equal to zero. From here we can redefine divide, which will prevent zero from being passed in.

const divide =  def(
'divide',
{},
[$.Number, NonZeroInteger, $.Number],
divideImpl
)

divide(10)(0)

/*
TypeError: Invalid value

divide :: Number -> NonZeroInteger -> Number
^^^^^^^^^^^^^^
1

1) 0 :: Number

The value at position 1 is not a member of ‘NonZeroInteger’.
*/

It’s a beautiful thing.

If you are interested in exploring functional programming, please reach out. co me code with me.

Originally published at https://www.linkedin.com.

--

--

Todd Brown
Nerd For Tech

A 25 year software industry veteran with a passion for functional programming, architecture, mentoring / team development, xp/agile and doing the right thing.