Evitar esos dang no se puede leer la propiedad de errores indefinidos | Programar Plus

Uncaught TypeError: Cannot read property 'foo' of undefined.El temido error que todos encontramos en algún momento del desarrollo de JavaScript. Podría ser un estado vacío de una API que regresa de manera diferente a lo esperado. Podría ser otra cosa. No lo sabemos porque el error en sí es tan general y amplio.

Recientemente tuve un problema en el que ciertas variables de entorno no estaban siendo ingresadas por una razón u otra, causando todo tipo de extrañeza con ese error mirándome a la cara. Cualquiera que sea la causa, puede ser un error desastroso si no se tiene en cuenta, entonces, ¿cómo podemos prevenirlo en primer lugar?

Vamos a averiguarlo.

Biblioteca de utilidades

Si ya está utilizando una biblioteca de utilidades en su proyecto, es muy probable que incluya una función para prevenir este error. _.get​ en lodash (docs) o R.path en Ramda (docs) permiten acceder al objeto de forma segura.

Si ya está utilizando una biblioteca de utilidades, probablemente esta sea la solución más simple. Si no está utilizando una biblioteca de utilidades, ¡siga leyendo!

Cortocircuito con &&

Un hecho interesante sobre los operadores lógicos en JavaScript es que no siempre devuelven un booleano. Según la especificación, “el valor producido por un &&​ o ||El operador no es necesariamente de tipo booleano. El valor producido siempre será el valor de una de las dos expresiones de operando “.

En el caso de &&Operador, la primera expresión se utilizará si es un valor “falso”. De lo contrario, se utilizará la segunda expresión. Esto significa que la expresión 0 && 1Será evaluado como 0(Un valor falso) y la expresión 2 && 3Será evaluado como 3​. Si es múltiple &&Las expresiones están encadenadas juntas, se evaluarán al primer valor falso o al último valor. Por ejemplo, 1 && 2 && 3 && null && 4​ evaluará a nullY 1 && 2 && 3Evaluará a 3.

¿Qué utilidad tiene esto para acceder de forma segura a las propiedades de los objetos anidados? Los operadores lógicos en JavaScript se “cortocircuitarán”. En este caso de &&, Esto significa que la expresión dejará de avanzar después de que alcance su primer valor falso.

​​const foo = false && destroyAllHumans();
​​console.log(foo); // false, and humanity is safe

En este ejemplo, destroyAllHumans nunca se llama porque el &&Operando detuvo todas las evaluaciones después de falso.

Esto se puede utilizar para acceder de forma segura a las propiedades anidadas.

​​const meals = {
​​  breakfast: null, // I skipped the most important meal of the day! :(
​​  lunch: {
​​    protein: 'Chicken',
​​    greens: 'Spinach',
​​  },
​​  dinner: {
​​    protein: 'Soy',
​​    greens: 'Kale',
​​  },
​​};
​​
​​const breakfastProtein = meals.breakfast && meals.breakfast.protein; // null
​​const lunchProtein = meals.lunch && meals.lunch.protein; // 'Chicken'

Aparte de su sencillez, una de las principales ventajas de este enfoque es su brevedad cuando se trata de pequeñas cadenas. Sin embargo, al acceder a objetos más profundos, esto puede resultar bastante detallado.

const favorites = {
​​  video: {
​​    movies: ['Casablanca', 'Citizen Kane', 'Gone With The Wind'],
​​    shows: ['The Simpsons', 'Arrested Development'],
​​    vlogs: null,
​​  },
​​  audio: {
​​    podcasts: ['Shop Talk Show', 'CodePen Radio'],
​​    audiobooks: null,
​​  },
​​  reading: null, // Just kidding -- I love to read
​​};
​​
​​const favoriteMovie = favorites.video && favorites.video.movies && favorites.video.movies[0];
​​// Casablanca
​​const favoriteVlog = favorites.video && favorites.video.vlogs && favorites.video.vlogs[0];
​​// null

Cuanto más profundamente anidado está un objeto, más difícil de manejar se vuelve.

La “quizás mónada”

A Oliver Steele se le ocurrió este método y lo analiza con mucho más detalle en su publicación de blog, “Monads on the Cheap I: The Maybe Monad”. Intentaré dar una breve explicación aquí.

const favoriteBook = ((favorites.reading||{}).books||[])[0]; // undefined
​​const favoriteAudiobook = ((favorites.audio||{}).audiobooks||[])[0]; // undefined
​​const favoritePodcast = ((favorites.audio||{}).podcasts||[])[0]; // 'Shop Talk Show'

Al igual que en el ejemplo de cortocircuito anterior, este método funciona comprobando si un valor es falso. Si es así, intentará acceder a la siguiente propiedad en un objeto vacío. En el ejemplo anterior, favorite.reading es nulo, por lo que se accede a la propiedad books desde un objeto vacío. Esto dará como resultado un indefinido, por lo que también se accederá al 0 desde una matriz vacía.

La ventaja de este método sobre el &&El método es que evita la repetición de nombres de propiedad. En objetos más profundos, esto puede ser una ventaja bastante significativa. La principal desventaja sería la legibilidad: no es un patrón común y el lector puede necesitar un momento para analizar cómo está funcionando.

Intentar / atrapar

​​try...catchLas declaraciones en JavaScript permiten otro método para acceder de forma segura a las propiedades.

try {
​​  console.log(favorites.reading.magazines[0]);
​​} catch (error) {
​​  console.log("No magazines have been favorited.");
​​}

Desafortunadamente, en JavaScript, try...catchLas declaraciones no son expresiones. No evalúan a un valor como lo hacen en algunos idiomas. Esto evita una declaración try concisa como una forma de establecer una variable.

Una opción es utilizar una variable let que se define en el bloque encima de la try...catch.

let favoriteMagazine;
​​try { 
​​  favoriteMagazine = favorites.reading.magazines[0]; 
​​} catch (error) { 
​​  favoriteMagazine = null; /* any default can be used */
​​};

Aunque es detallado, esto funciona para configurar una sola variable (es decir, si la variable mutable no lo asusta). Sin embargo, pueden surgir problemas si se hacen a granel.

let favoriteMagazine, favoriteMovie, favoriteShow;
​​try {
​​  favoriteMovie = favorites.video.movies[0];
​​  favoriteShow = favorites.video.shows[0];
​​  favoriteMagazine = favorites.reading.magazines[0];
​​} catch (error) {
​​  favoriteMagazine = null;
​​  favoriteMovie = null;
​​  favoriteShow = null;
​​};
​​
​​console.log(favoriteMovie); // null
​​console.log(favoriteShow); // null
​​console.log(favoriteMagazine); // null

Si alguno de los intentos de acceder a la propiedad falla, esto hará que todos vuelvan a sus valores predeterminados.

Una alternativa es envolver el try...catchEn una función de utilidad reutilizable.

const tryFn = (fn, fallback = null) => {
​​  try {
​​    return fn();
​​  } catch (error) {
​​    return fallback;
​​  }
​​} 
​​
​​const favoriteBook = tryFn(() => favorites.reading.book[0]); // null
​​const favoriteMovie = tryFn(() => favorites.video.movies[0]); // "Casablanca"

Al envolver el acceso al objeto en una función, puede retrasar el código “inseguro” y pasarlo a un try...catch.

Una gran ventaja de este método es lo natural que resulta acceder a la propiedad. Siempre que las propiedades estén incluidas en una función, se puede acceder a ellas de forma segura. También se puede especificar un valor predeterminado en el caso de una ruta inexistente.

Fusionar con un objeto predeterminado

Al fusionar un objeto con un objeto de forma similar de “valores predeterminados”, podemos asegurarnos de que la ruta a la que intentamos acceder sea segura.

const defaults = {
​​  position: "static",
​​  background: "transparent",
​​  border: "none",
​​};
​​
​​const settings = {
​​  border: "1px solid blue",
​​};
​​
​​const merged = { ...defaults, ...settings };
​​
​​console.log(merged); 
​​/*
​​  {
​​    position: "static",
​​    background: "transparent",
​​    border: "1px solid blue"
​​  }
​​*/

Sin embargo, tenga cuidado porque se puede sobrescribir todo el objeto anidado en lugar de una sola propiedad.

const defaults = {
​​  font: {
​​    family: "Helvetica",
​​    size: "12px",
​​    style: "normal",
​​  },        
​​  color: "black",
​​};
​​
​​const settings = {
​​  font: {
​​    size: "16px",
​​  }
​​};
​​
​​const merged = { 
​​  ...defaults, 
​​  ...settings,
​​};
​​
​​console.log(merged.font.size); // "16px"
​​console.log(merged.font.style); // undefined

¡Oh no! Para solucionar esto, necesitaremos copiar de manera similar cada uno de los objetos anidados.

const merged = { 
​​  ...defaults, 
​​  ...settings,
​​  font: {
​​    ...defaults.font,
​​    ...settings.font,
​​  },
​​};
​​
​​console.log(merged.font.size); // "16px"
​​console.log(merged.font.style); // "normal"

¡Mucho mejor!

Este patrón es común con complementos o componentes que aceptan un objeto de configuración grande con valores predeterminados incluidos.

Una ventaja de este enfoque es que, al escribir un objeto predeterminado, incluimos documentación sobre cómo debería verse un objeto. Desafortunadamente, dependiendo del tamaño y la forma de los datos, la “fusión” puede estar llena de copias de cada objeto anidado.

El futuro: encadenamiento opcional

Actualmente existe una propuesta de TC39 para una función llamada “encadenamiento opcional”. Este nuevo operador se vería así:

​​console.log(favorites?.video?.shows[0]); // 'The Simpsons'
​​console.log(favorites?.audio?.audiobooks[0]); // undefined

El ?.El operador trabaja por cortocircuito: si el lado izquierdo del ?.Operador evalúa a nullO undefined, La expresión completa se evaluará como undefinedY el lado derecho permanecerá sin evaluar.

Para tener un valor predeterminado personalizado, podemos usar el ||Operador en el caso de un indefinido.

console.log(favorites?.audio?.audiobooks[0] || "The Hobbit");

¿Qué método deberías utilizar?

La respuesta, como habrás adivinado, es esa antigua respuesta … “depende”. Si el operador de encadenamiento opcional se ha agregado al idioma y tiene el soporte de navegador necesario, probablemente sea la mejor opción. Sin embargo, si no eres del futuro, hay más consideraciones a tener en cuenta. ¿Está utilizando una biblioteca de utilidades? ¿Qué tan profundamente anidado está tu objeto? ¿Necesita especificar valores predeterminados? Los diferentes casos pueden justificar un enfoque diferente.