DEV Community

Cover image for Eliminating Partial Functions by Balancing Types in TypeScript
Jethro Larson
Jethro Larson

Posted on • Edited on

Eliminating Partial Functions by Balancing Types in TypeScript

When writing software it's valuable to avoid code that throws exceptions as they lead to problems that are costly, complicate the code, and are hard to debug. Functions that don't return valid results for all valid inputs are called "partial functions". The better option is to create "total functions". In typed languages "valid" is encoded in the type, so for a function from number[] => number to be total there must not exist any array of numbers that causes the function to not return a number. Let's look at a counter example.

const headNum = (xs: number[]): number => xs[0];
Enter fullscreen mode Exit fullscreen mode

This function doesn't return a number when passed an empty array. In that case it will return undefined. This breaks the contract of the function. It's disappointing that TypeScript doesn't make this a type error but this can be overcome in a few ways.

Weaken the return type

The first step is always to make the types not lie.

const headNum = (xs: number[]): number | undefined => xs[0];
Enter fullscreen mode Exit fullscreen mode

This succeeds in making the function total, but now it's harder to compose with other functions dealing with numbers.

const suc = (n: number): number => n + 1;

suc(headNum([1])); // => Type Error
Enter fullscreen mode Exit fullscreen mode

The caller of headNum now has to guard against undefined to use it.

Encode the weakness in another type

Rather than encoding the weakness in a union a type can be used to represent the failure. In this case the Option type is a good choice.

type Option<T> = None | Some<T>;
type None = {tag: 'None'};
type Some<T> = {tag: 'Some', val: T};

const none: None = {tag: 'none'};
const some: <T>(val: T): Option<T> => {tag: 'Some', val};
Enter fullscreen mode Exit fullscreen mode

Now change headNum to return Option<number>.

const headNum = (xs: number[]): Option<number> =>
  xs.length ? some(xs[0]) : none;
Enter fullscreen mode Exit fullscreen mode

However this hasn't yet increased the usability over simply doing the union with undefined. A way of composing functions with values of this type is needed:

const mapOption = <T, U>(fn: (x: T) => U, o: Option<T>): Option<U> => {
  switch(o.tag){
    case 'None': return none;
    case 'Some': return some(fn(o.val));
  }
};
Enter fullscreen mode Exit fullscreen mode

And now suc can be more easily composed with headNum and we remain confident that there won’t be exceptions.

mapOption(suc, headNum([1])); // => Some(2)
mapOption(suc, headNum([])); // => none
Enter fullscreen mode Exit fullscreen mode

There's a lot more to the Option type (AKA "Maybe"). Check out libraries like fp-ts for more info.

Provide a fall-back

Rather than adjusting the return types we can choose to guard on the leading side. The simplest way is to accept the fallback value as an argument. This is not as flexible as using an Option but is great in a lot of cases, and easy to understand for most developers.

const headNum = (fallback: number, xs: number[]): number =>
  xs.length ? xs[0] : fallback;
Enter fullscreen mode Exit fullscreen mode

Usage:

suc(headNum(1, [])); // => 1
Enter fullscreen mode Exit fullscreen mode

The trade-off here is that it's harder to do something vastly different in the failure case as the failure is caught in advance.

Strengthen argument type

The last tactic I want to cover is strengthening the argument type so that there are no inputs which produce invalid numbers. In this case a type for an non-empty array is needed:

type NonEmptyArray<T> = [T, T[]]; 
const nonEmpty = <T>(x: T, xs: T[]): NonEmptyArray<T> => [x, xs];
Enter fullscreen mode Exit fullscreen mode

headNum then becomes:

const headNum = (xs: NonEmptyArray<number>): number =>
  xs[0]
Enter fullscreen mode Exit fullscreen mode

And usage:

suc(headNum(nonEmpty(1, [])));
Enter fullscreen mode Exit fullscreen mode

Notice how similar this is to the fall-back approach. The difference is that NonEmptyArray is now a proper type and it can be reused in other ways. Employing a library like fp-ts will help get the full benefits of this tactic.

Conclusion

As I have demonstrated, there's a few options for dealing with weaknesses in function types. To make functions total the return type can be weakened or the argument types can be strengthened. I strongly encourage you to play with them next time you identify a partial function in your application.

Friends don't let friends write partial functions.

Further reading

Update: TypeScript 4.1 added noUncheckedIndexedAccess compiler option to close the gap on accessing array items unsafely.

Top comments (0)