By Eric Elliott

Translation: Crazy geek

Original text: medium.com/javascript-…

Reproduced without permission

Image credit: NASA/CXC/M.Weiss

Many JavaScript developers are struggling with what to do with optional values. Is there a good way to minimize errors caused by values that may be null, undefined, or uninitialized at run time?

In some cases, some languages have built-in capabilities. In some statically typed languages, you can say null and undefined are illegal values and have your programming language throw TypeError at compile time, but even in those languages, there is nothing to prevent null input from flowing into your program at run time.

To better deal with such problems, we need to understand the source of these values. Here are some of the most common sources:

  • User input
  • Database/network records
  • Uninitialized state
  • Does the function return nothing

User Input

User input

Validating user input is the first and best line of defense when working with it. I often rely on schema validators to do this. For example, check the react-jsonSchema-form.

Input from stream record

I always get input from the network, database, or stream record of user input. For example, I could combine user records with Redux Action Creators, which handled the undefined value:

const setUser = ({ name = 'Anonymous', avatar = 'anon.png' } = {}) = > ({
  type: setUser.type,
  payload: {
    name,
    avatar
  }
});
setUser.type = 'userReducer/setUser';
Copy the code

Sometimes, you need to display different content based on the current state of the data. You might encounter this if the page is displayed before all the data is initialized. For example, when you show the user a money balance, you might accidentally display a balance of $0 before loading the data, which would upset the user. You can create custom data types that produce different output depending on the current state:

const createBalance = ({
  // default state
  state = 'uninitialized',
  value = createBalance.empty
} = {}) => ({
  __proto__: {
    uninitialized: (a)= > The '-'.initialized: (a)= > value,
    format () {
      return this[this.getState()](value);
    },
    getState: (a)= > state,
    set: value= > {
      const test = Number(value);
      assert(!Number.isNaN(test), `setBalance Invalid value: ${ value }`);
      return createBalance({
        state: 'initialized', value }); }}}); createBalance.empty ='0';const setBalance = value= > createBalance().set(value);const emptyBalanceForDisplay = createBalance()
  .format();
console.log(emptyBalanceForDisplay); // '--'const balanceForDisplay = setBalance('25')
  .format(balance);
console.log(balanceForDisplay); // '25'// Uncomment these calls to see the error cases:
// setBalance('foo'); // Error: setBalance Invalid value: foo// Error: createBalance Invalid state: THIS IS NOT VALID
// createBalance({ state: 'THIS IS NOT VALID', value: '0' });
Copy the code

The code above is a state machine and does not display invalid states. When the balance is first created, it will be set to the uninitialized state. If you try to display the balance while in the uninitialized state, you will always get a placeholder value (” — “).

To change this value, you must explicitly set a value by calling the.set method or setBalance defined in the createBalance factory.

The state itself is encapsulated to protect it from external interference, and to ensure that other functions cannot capture it and set it to an invalid state.

Note: You may be wondering why I’m using strings instead of numbers as an example, but that’s because using large strings to represent currency types has decimal precision, avoids rounding errors, and accurately represents the value of cryptocurrency transactions, resulting in arbitrary valid decimal precision.

If you use Redux or the Redux architecture, you can declare the state machine with Redux-DSM.

Avoid creatingnullundefined

You can avoid creating null or undefined values in your own functions in the first place. I came up with a number of ways to build JavaScript. See below.

Avoid null

I’ve never explicitly created a null value in JavaScript, because I’ve never really seen what it means.

Since 2015, JavaScript has supported default values, which are filled in when you don’t provide a value for a parameter or attribute. These defaults do not apply to null values. In my experience, this is usually a mistake. To avoid this trap, do not use NULL in JavaScript.

If you want to use special cases for uninitialized or null values, a state machine is a better choice.

New JavaScript features

There are several features to help you deal with nuL or undefined values. As of this writing, both are phase 3 recommendations. Maybe you can use them in the future.

At the time of this writing, the optional link is a phase 3 recommendation. It goes like this:

const foo = {};
// console.log(foo.bar.baz); // throws error
console.log(foo.bar? .baz)// undefined
Copy the code

The void merge operator

The “void merge operator” is also a phase 3 recommendation to be added to the specification, which is basically a fancy way of “fallback value operators”. If the value on the left is undefined or null, it is evaluated to the value on the right. It goes like this:

let baz;
console.log(baz); // undefined
console.log(baz ?? 'default baz');
// default baz// Combine with optional chaining:
console.log(foo.bar? .baz ??'default baz');
// default baz
Copy the code

If the future does not come, install @babel/plugin-proposal-optional-chaining and @babel/ plugin-proposal-Nullish -coalescing-operator.

Asynchronous and Promise

If a function may not return a value, it is best to wrap it in Either. In functional programming, Either Monad is a special abstract data type that allows you to attach two different code paths: a success path or a failure path. JavaScript has a built-in asynchronous Either Monad-ish data type called Promise. You can use it to declarative error branching for undefined values:

const exists = x= >x ! =null;const ifExists = value= > exists(value) ?
  Promise.resolve(value) :
  Promise.reject(`Invalid value: ${ value }`); ifExists(null).then(log).catch(log); // Invalid value: null
ifExists('hello').then(log).catch(log); // hello
Copy the code

You can write a synchronized version as needed, but I’ll leave it to you as an exercise. If you are familiar with functors and Monads, the process becomes easier; If that sounds daunting, don’t worry, just use promises. They are built in and work just fine in most cases.

Maybe the array

Arrays implement a map method that takes a function applied to each element array. If the array is empty, the function is never called. In other words, arrays in JavaScript can fill the role of Maybe in languages like Haskell.

What is Maybe?

Maybe is a special abstract data type that encapsulates an optional value. Data types come in two forms:

  • Just – contains a value
  • Nothing – has no value

The core idea is this:

const log = x= > console.log(x);
const exists = x= >x ! =null;const Just = value= > ({
  map: f= > Just(f(value)),
});const Nothing = (a)= > ({
  map: (a)= > Nothing(),
});const Maybe = value= > exists(value) ?
  Just(value) :
  Nothing();const empty = undefined;
Maybe(empty).map(log); // does not log
Maybe('Maybe Foo').map(log); // logs "Maybe Foo"
Copy the code

The above is just an example to illustrate the concept. You can build an entire library of useful functions around Maybe to implement other operations, such as flatMap and flat (avoid Just(Just(value)) when writing multiple Maybe return functions). But JavaScript already has a data type that can do this directly: arrays.

This is a good example if you are creating a function that may or may not produce a result (especially if it may have multiple results).

const log = x= > console.log(x);
const exists = x= >x ! =null;const arr = [1.2.3];
const find = (p, list) = > [list.find(p)].filter(exists);
find(x= > x > 3, arr).map(log); // does not log anything
find(x= > x < 3, arr).map(log); // logs 1
Copy the code

I find it useful not to call map on an empty list to avoid null and undefined values, but remember that if an array contains null and undefined values, it will call a function to handle those values, so, If your function might produce null or undefined, filter it out of the returned array. That might change the length of the set.

In Haskell, there is a function maybe (similar to Map) that applies a function to a value. But the value is optional and encapsulated in Maybe. We can do the same thing with JavaScript’s Array data type:

// maybe = b => (a => b) => [a] => b const maybe = (fallback, f = () => {}) => arr => arr.map(f)[0] || fallback; // turn a value (or null/undefined) into a maybeArray const toMaybeArray = value => [value].filter(exists); // maybe multiply the contents of an array by 2, // default to 0ifthe array is empty const maybeDouble = maybe(0, x => x * 2); const emptyArray = toMaybeArray(null); const maybe2 = toMaybeArray(2); // logs:"maybeDouble with fallback: 0"
console.log('maybeDouble with fallback: ', maybeDouble(emptyArray));
// logs: "maybeDouble(maybe2): 4"
console.log('maybeDouble(maybe2): ', maybeDouble(maybe2));
Copy the code

Maybe uses a backup value, then a function that maps to the May array, then a May array (an array that contains a value, or nothing), and then returns the result of applying the function to the contents of the array, or returns the value if the array is empty.

For convenience, I’ve also defined a toMaybeArray function and added a Maybe function to demonstrate it.

If you want to do something similar in your production code, I’ve created a unit-tested open source library called Maybearray to make it easier. The advantage of Maybearray over other JavaScript Maybe libraries is that it uses native JavaScript arrays to represent values, so you don’t have to do anything special or transform them. When you come across the Maybe array in debugging, you don’t have to ask “What strange type is this? !” It’s just an array of values or an empty array, which you’ve seen a million times.

Welcome to pay attention to the front end public number: front end pioneer, free front-end engineering utility kit.