Why are we wrapping our code in more and more tools if at the end of the day it all just runs in JavaScript -- A Colleague
This is a fair question. The goal at the end of the day for any engineer is to execute work that brings the business value. And we hope to do with mastery, autonomy, and purpose. [Pink 2009]
You can follow along with the code here: https://codesandbox.io/s/why-javascript-isnt-enough-0q783
Let's say we have a user. That user has a name, sometimes We'll ignore where we got his user for this post.
const jane = { name: 'jane', }; const noName = { name: undefined, };
Alright, let's write a function to say hello.
const sayHello = (name) => `hello, ${name}`;
And use that function:
const log = console.log; log(sayHello(jane.name)); // 'hello, jane' log(sayHello(noName.name)); // 'hello, undefined'
Well, thats about what we'd expect.
Say we don't want to print out 'hello, undefined'
if the user doesn't have a name.
One way to do this is to add some protections to our sayHello
function.
const sayHelloProtected = (name) => { if (name === undefined) { return 'cant say hello without a name'; } return `hello, ${name}`; };
Let's see how that works:
log(sayHelloProtected(jane.name)); // 'hello, jane' log(sayHelloProtected(noName.name)); // 'can't say hello without a name'
Progress! Here is where a lot of us hop back into work and go along. We can go a long way with this level of programming.
At some point we might experience a moment of friction. We get an error where undefined
crashes production. We get a user complaint about a UI that has undefined
displaying to them. We might annoyed that we need to keep writing guards to protect against possibly undefined values in our code. We might get annoyed that we are now writing unit tests to check if we handle undefined
well, everywhere.
One or any of these friction points might cause us to ask, is this the best we can do?
We hear people talking about TypeScript, we hear that popular libraries are starting to be written in TypeScript. Is it hype? Or does it have staying power?
Let's add some types to our code. We're going to start back with our original sayHello
function and add a type on the input:
const sayHelloTyped = (name: string) => `hello, ${name}`;
Ok, that wasn't too crazy. We told the TypeScript compiler that our function sayHelloTyped
takes an argument that is a string.
Now we can tell the TypeScript compiler about the shapes that our user objects can take:
type User = { name: string | undefined; };
Here we've said that a User
has a field with key name
and the value of that field is either a string
or undefined
.
Now we can tell the compiler that our users are of this type:
const jane2: User = { name: 'jane', }; const noName2: User = { name: undefined, };
If you open codesandbox or setup your editor and write
log(sayHelloTyped(jane2.name));
We'll get red swiggles as the compiler has predicted an error that could happen!
Wonderful! We have the computer thinking ahead for us and letting us know that we need to make sure to handle the case where the User
name
is undefined
. Let's do that:
const sayHelloTyped2 = (name: string | undefined) => `hello, ${name}`;
Lets see what our editor thinks about that with noName
:
Uh oh. No red swiggles, TypeScript is happy, but we are about to get our 'hello, undefined'
error back. Template literals are typed to allow undefined
.
This is actually an open issue on the TypeScript project: https://github.com/microsoft/TypeScript/issues/30239
Even if TypeScript was updated so that template literals didn't allow undefined
we'd still run into the problem, that our colleague pointed out, where our code compiles down to JavaScript stripping away any type safety.
What are we to do?
Option
sLuckily we're not the first people to run into this issue. From functional programming world there is a thing that can describe to the compiler and in runtime our situation.
In this post we are going to use a library called fp-ts. It contains many useful modules for writing in typed functional programming style. Today we are going look at one module.
Welcome to the Option
module and type. You can think of the option
as an object that either has something or has nothing. If we describe our User
with this we'd write:
import * as O from 'fp-ts/lib/Option'; type UserSafe = { name: O.Option<string>; };
Here we've said that the name
field has a value of option
that contains a string.
Let's constructor our users to match this type:
const janeSafe: UserSafe = {
name: O.some('jane')
};
const noFaceSafe: UserSafe = {
name: O.none
};
Now if write out our usual log, we'll get a red swiggle type error:
Now how do we deal with this? One way is teaching sayHelloTyped
how to handle option
s. But this mixes concerns. This function is beautiful and simple right now. Another way is to create a new function that uses sayHelloTyped
.
We're gonna bust out some functional programming here that we've not introduced yet. We can read through this and still get a sense of what is happening:
import { pipe } from 'fp-ts/lib/function'; const sayHelloSafely = (name: OptionT<string>) => pipe( name, Option.map(sayHelloTyped), Option.getOrElse(() => 'cant say hello without a name'), );
Let's talk through sayHelloSafely
. This function takes in a name
that is now an option
that can contain a string
.
Whats this pipe
thing? It similar to the "pipe" in bash written as |
.
It allows us stack functions where the output of one becomes the input of another.
Its hard to read but here is what sayHelloSafely
would look like if we wrote it without the pipe
:
const sayHelloSafely2 = (name: O.Option<string>) => O.getOrElse(() => 'cant say hello without a name')( O.map(sayHelloTyped)(name), );
What's this O.map
? This lets reuse any function doesn't take option
and use it as if it did.
What's this O.getOrElse
? This lets us handle the case where there is no name in a way that is separate from our original sayHelloTyped
function.
All together now, we get: https://codesandbox.io/s/why-javascript-isnt-enough-0q783
const janeSafe: UserSafe = { name: O.some('jane'), }; const noFaceSafe: UserSafe = { name: O.none, }; const sayHelloTyped = (name: string) => `hello, ${name}`; const sayHelloSafely = (name: O.Option<string>) => pipe( name, O.map(sayHelloTyped), O.getOrElse(() => 'cant say hello without a name'), ); log(sayHelloSafely(janeSafe.name)); // 'hello, jane' log(sayHelloSafely(noFaceSafe.name)); // 'cant say hello without a name'
Kinda weird, kinda cool. We've taught the JavaScript runtime a new trick. Before we expressed "none" via undefined
or null
. Now we express "none" via Option.none
. And we have these functional programming legos of pipe
, Option.map
, Option.getOrElse
to allow us to handle non-option aware code to use option
s.
The goal at the end of the day for any engineer is to execute work that brings the business value. And we hope to do with mastery, autonomy, and purpose. [Pink 2009]
Bugs are anti-value. Types and functional programming reduce bugs by forcing errors from runtime to compile time.
Business changes fast which requires software changes. When software changes we have more opportunities for bugs. Types and functional programming allow your editor and build tools to tell you if that change is gonna break anything else both it happens in runtime.
The mind can only think about so much at once. Separating concerns into bite-size pieces allows us to think clearly. Once those bite-size pieces are clear, we can lean on types and functional programming to combine them in a way that will ensure they continue to work as expected.
JavaScript runs where our users are. Types and functional programming allows us to write programs we are more sure will run the way we want for our users.
Pink, Daniel. 2009. Drive: The Surprising Truth About What Motivates Us
I love working with fp-ts BUT I think the jump into it toward the end a jump higher in pace that is out of place with the beginning of the post. If I make time, I'll try to refactor this post to write its own Option
and not use any outside libraries.