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];
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];
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
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};
Now change headNum
to return Option<number>
.
const headNum = (xs: number[]): Option<number> =>
xs.length ? some(xs[0]) : none;
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));
}
};
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
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;
Usage:
suc(headNum(1, [])); // => 1
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];
headNum
then becomes:
const headNum = (xs: NonEmptyArray<number>): number =>
xs[0]
And usage:
suc(headNum(nonEmpty(1, [])));
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
- Partial Function on Wikipedia
- Parse, Don't Validate My original inspiration
- Type Safety Back and Forth
-
fp-ts Functional TS library with
Option
andNonEmptyArray
types and more
Update: TypeScript 4.1 added noUncheckedIndexedAccess compiler option to close the gap on accessing array items unsafely.
Top comments (0)