Git Pathspecs and How to Use Them

When I was looking through the documentation of git commands, I noticed that many of them had an option for <pathspec>. I initially thought that this was just a technical way to say “path,” and assumed that it could only accept directories and filenames. After diving into the rabbit hole of documentation, I found that the pathspec option of git commands are capable of so much more.

The pathspec is the mechanism that git uses for limiting the scope of a git command to a subset of the repository. If you have used much git, you have likely used a pathspec whether you know it or not. For example, in the command git add README.md, the pathspec is README.md. However, it is capable of much more nuance and flexibility.

So, why should you learn about pathspecs? Since it is a part of many commands, these commands become much more powerful with an understanding of pathspecs. With git add, you can add just the files within a single directory. With git diff, you can examine just the changes made to filenames with an extension of .scss. You can git grep all files except for those in the /dist directory.

In addition, pathspecs can help with the writing of more generic git aliases. For example, I have an alias named git todo, which will search all of my repository files for the string 'todo'. However, I would like for this to show all instances of the string, even if they are not within my current working directory. With pathspecs, we will see how this becomes possible.

File or directory

The most straightforward way to use a pathspec is with just a directory and/or filename. For example, with git add you can do the following. ., src/, and README are the respective pathspecs for each command.

git add .      # add CWD (current working directory)
git add src/   # add src/ directory
git add README # add only README directory

You can also add multiple pathspecs to a command:

git add src/ server/ # adds both src/ and server/ directories

Sometimes, you may see a -- preceding the pathspec of a command. This is used to remove any ambiguity of what is the pathspec and what is part of the command.

Wildcards

In addition to files & directories, you can match patterns using *, ?, and []. The * symbol is used as a wildcard and it will match the / in paths — in other words, it will search through subdirectories.

git log '*.js' # logs all .js files in CWD and subdirectories
git log '.*'   # logs all 'hidden' files and directories in CWD
git log '*/.*' # logs all 'hidden' files and directories in subdirectories

The quotes are important, especially when using *! They prevent your shell (such as bash or ZSH) from attempting to expand the wildcards on their own. For example, let’s take a look at how git ls-files will list files with and without the quotes.

# example directory structure
#
# .
# ├── package-lock.json
# ├── package.json
# └── data
#     ├── bar.json
#     ├── baz.json
#     └── foo.json

git ls-files *.json 

# package-lock.json
# package.json

git ls-files '*.json'

# data/bar.json
# data/baz.json
# data/foo.json
# package-lock.json
# package.json

Since the shell is expanding the * in the first command, git ls-files receives the command as git ls-files package-lock.json package.json. The quotes ensure that git is the one to resolve the wildcard.

You can also use the ? character as a wildcard for a single character. For example, to match either mp3 or mp4 files, you can do the following.

git ls-files '*.mp?'

Bracket expressions

You can also use “bracket expressions” to match a single character out of a set. For example, if you'd like to make matches between either TypeScript or JavaScript files, you can use [tj]. This will match either a t or a j.

git ls-files '*.[tj]s'

This will match either .ts files or .js files. In addition to just using characters, there are certain collections of characters that can be referenced within bracket expressions. For example, you can use [:digit:] within a bracket expression to match any decimal digit, or you can use [:space:] to match any space characters.

git ls-files '*.mp[[:digit:]]' # mp0, mp1, mp2, mp3, ..., mp9
git ls-files '*[[:space:]]*' # matches any path containing a space

To read more about bracket expression and how to use them, check out the GNU manual.

Magic signatures

Pathspecs also have the special tool in their arsenal called “magic signatures” which unlock some additional functionality to your pathspecs. These “magic signatures” are called by using :(signature) at the beginning of your pathspec. If this doesn't make sense, don't worry: some examples will hopefully help clear it up.

top

The top signature tells git to match the pattern from the root of the git repository rather than the current working directory. You can also use the shorthand :/ rather than :(top).

git ls-files ':(top)*.js'
git ls-files ':/*.js' # shorthand

This will list all files in your repository that have an extension of .js. With the top signature this can be called within any subdirectory in your repository. I find this to be especially useful when writing generic git aliases!

git config --global alias.js 'ls-files -- ':(top)*.js''

You can use git js anywhere within your repository to get a list of all JavaScript files in your project using this.

icase

The icase signature tells git to not care about case when matching. This could be useful if you don't care which case the filename is — for example, this could be useful for matching jpg files, which sometimes use the uppercase extension JPG.

git ls-files ':(icase)*.jpg'

literal

The literal signature tells git to treat all of your characters literally. This would be used if you want to treat characters such as * and ? as themselves, rather than as wildcards. Unless your repository has filenames with * or ?, I don't expect that this signature would be used too often.

git log ':(literal)*.js' # returns log for the file '*.js'

glob

When I started learning pathspecs, I noticed that wildcards worked differently than I was used to. Typically I see a single asterisk * as being a wildcard that does not match anything through directories and consecutive asterisks (**) as a “deep” wildcard that does match names through directories. If you would prefer this style of wildcards, you can use the glob magic signature!

This can be useful if you want more fine-grained control over how you search through your project’s directory structure. As an example, take a look at how these two git ls-files can search through a React project.

git ls-files ':(glob)src/components/*/*.jsx' # 'top level' jsx components
git ls-files ':(glob)src/components/**/*.jsx' # 'all' jsx components

attr

Git has the ability to set “attributes” to specific files. You can set these attributes using a .gitattributes file.

# .gitattributes

src/components/vendor/*  vendored # sets 'vendored' attribute
src/styles/vendor/*      vendored

Using the attr magic signature can set attribute requirements for your pathspec. For example, we might want to ignore the above files from a vendor.

git ls-files ':(attr:!vendored)*.js' # searches for non-vendored js files
git ls-files ':(attr:vendored)*.js'  # searches for vendored js files

exclude

Lastly, there is the “exclude'” magic signature (shorthand of :! or :^). This signature works differently from the rest of the magic signatures. After all other pathspecs have been resolved, all pathspecs with an exclude signature are resolved and then removed from the returned paths. For example, you can search through all of your .js files while excluding the .spec.js test files.

git grep 'foo' -- '*.js' ':(exclude)*.spec.js' # search .js files excluding .spec.js
git grep 'foo' -- '*.js' ':!*.spec.js' .       # shorthand for the same

Combining signatures

There is nothing limiting you from using multiple magic signatures in a single pathspec! You can use multiple signatures by separating your magic words with commas within your parenthesis. For example, you can do the following if you’d like to match from the base of your repository (using top), case insensitively (using icase), using only authored code (ignoring vendor files with attr), and using glob-style wildcards (using glob).

git ls-files -- ':(top,icase,glob,attr:!vendored)src/components/*/*.jsx'

The only two magic signatures that you are unable to combine are glob and literal, since they both affect how git deals with wildcards. This is referenced in the git glossary with perhaps my favorite sentence that I have ever read in any documentation.

Glob magic is incompatible with literal magic.


Pathspecs are an integral part of many git commands, but their flexibility is not immediately accessible. By learning how to use wildcards and magic signatures you can multiply your command of the git command line.

The post Git Pathspecs and How to Use Them appeared first on CSS-Tricks.

Level up your .sort game

Sorting is a super handy JavaScript method that can display the values of an array in a certain order. Whether that’s real estate listings by price, burger joints by distance, or best nearby happy hours by rating, sorting arrays of information is a common need.

If you’re already doing this with JavaScript on a project, you are will likely using the built-in array .sort method, which is in the same family of array methods that includes .filter, .map and .reduce.

Let's take a look at how to do that!

A quick note about side effects

Before going into the details of how to use .sort, there is a very important detail that needs to be addressed. While many of the ES5 array methods such as .filter, .map, and .reduce will return a new array and leave the original untouched, .sort will sort the array in place. If this is unwanted, an ES6 technique to avoid this is using the spread operator to concisely create a new array.

const foo = ['c','b','a'];
const bar = ['x','z','y'];
const fooSorted = foo.sort();
const barSorted = [...bar].sort();

console.log({foo, fooSorted, bar, barSorted});

/*
{
  "foo":       [ "a", "b", "c" ],
  "fooSorted": [ "a", "b", "c" ],
  "bar":       [ "x", "z", "y" ],
  "barSorted": [ "x", "y", "z" ]
}
*/

foo and fooSorted both reference the same array, but bar and barSorted are now individual arrays.

General overview

The only parameter of the .sort method is a function. The spec refers to this as the compareFn — I will refer to it as the "comparison function" for the rest of the post. This comparison function accepts two parameters, which I will refer to as a and b. a and b are the two elements that we will be comparing. If you do not provide a comparison function, the array will coerce each element into a string and sort according to Unicode points.

If you would like the a to be ordered first in the array, the comparison function should return a negative integer; for b, a positive integer. If you would like the two to maintain their current order, return a 0.

If you don't understand, don't worry! Hopefully it will become much more clear with a few examples.

Comparing numbers

One of the simplest callbacks to write is a number comparison.

const numbers = [13,8,2,21,5,1,3,1];
const byValue = (a,b) => a - b;
const sorted = [...numbers].sort(byValue);
console.log(sorted); // [1,1,2,3,5,8,13,21]

If a is greater than b, a - b will return a positive number, so b will be sorted first.

Comparing strings

When comparing strings, the > and < operators will compare values based on each string’s Unicode value. This means that all uppercase letters will be “less” than all lowercase letters, which can lead to unexpected behavior.

JavaScript does have a method to help with comparing strings: the String.prototype.localeCompare method. This method accepts a comparison string, a locale, and an options object. The options object accepts a few properties (all of which you can view here), but I find that the most useful is "sensitivity." This will affect how comparisons work between letter variations such as case and accent.

const strings = ['Über', 'alpha', 'Zeal', 'über', 'uber', 'Uber', 'Alpha', 'zeal'];

const sortBySensitivity = sensitivity => (a, b) => a.localeCompare(
  b,
  undefined, // locale string -- undefined means to use browser default
  { sensitivity }
);

const byAccent  = sortBySensitivity('accent');
const byBase    = sortBySensitivity('base');
const byCase    = sortBySensitivity('case');
const byVariant = sortBySensitivity('variant'); // default

const accentSorted  = [...strings].sort(byAccent);
const baseSorted    = [...strings].sort(byBase);
const caseSorted    = [...strings].sort(byCase);
const variantSorted = [...strings].sort(byVariant);

console.log({accentSorted, baseSorted, caseSorted, variantSorted});

/*
{
  "accentSorted":  [ "alpha", "Alpha", "uber", "Uber", "Über", "über", "Zeal", "zeal" ],
  "baseSorted":    [ "alpha", "Alpha", "Über", "über", "uber", "Uber", "Zeal", "zeal" ],
  "caseSorted":    [ "alpha", "Alpha", "über", "uber", "Über", "Uber", "zeal", "Zeal" ],
  "variantSorted": [ "alpha", "Alpha", "uber", "Uber", "über", "Über", "zeal", "Zeal" ]
}
*/

To me, baseSorted seems to be the most logical for most alphabetical sorting — ‘ü’, ‘u’, ‘Ü’, and ‘U’ are equivalent, so they remain in the order of the original array.

Running functions before comparing values

You may want to run a comparison function on a value that is derived from each array’s element. First, let’s write a comparison function factory that will "map" over the element before calling the comparison function.

const sortByMapped = (map,compareFn) => (a,b) => compareFn(map(a),map(b));

One use case for this is sorting based on the attribute of an object.

const purchases = [
  { name: 'Popcorn', price: 5.75 }, 
  { name: 'Movie Ticket', price: 12 },
  { name: 'Soda', price: 3.75 },
  { name: 'Candy', price: 5 },
];

const sortByMapped = (map,compareFn) => (a,b) => compareFn(map(a),map(b));
const byValue = (a,b) => a - b;
const toPrice = e => e.price;
const byPrice = sortByMapped(toPrice,byValue);

console.log([...purchases].sort(byPrice));

/*
[
  { name: "Soda", price: 3.75 },
  { name: "Candy", price: 5 },
  { name: "Popcorn", price: 5.75 },
  { name: "Movie Ticket", price: 12 }
]
*/

Another case might be to compare an array of dates.

const dates  = ['2018-12-10', '1991-02-10', '2015-10-07', '1990-01-11'];
const sortByMapped = (map,compareFn) => (a,b) => compareFn(map(a),map(b));
const toDate = e => new Date(e).getTime();
const byValue = (a,b) => a - b;
const byDate = sortByMapped(toDate,byValue);

console.log([...dates].sort(byDate));
// ["1990-01-11", "1991-02-10", "2015-10-07", "2018-12-10"]

Reversing a sort

There are some cases where you may want to reverse the outcome of a comparison function. This is subtly different than doing a sort and then reversing the result in the way ties are handled: if you reverse the outcome, ties will also be reversed in order.

To write a higher order function that accepts a comparison function and returns a new one, you will need to flip the sign of the comparison’s return value.

const flipComparison = fn => (a,b) => -fn(a,b);
const byAlpha = (a,b) => a.localeCompare(b, null, { sensitivity: 'base' });
const byReverseAlpha = flipComparison(byAlpha);

console.log(['A', 'B', 'C'].sort(byReverseAlpha)); // ['C','B','A']

Running a tiebreaker sort

There are times when you may want to have a "tie-breaker" sort — that is, another comparison function that is used in the case of a tie.

By using [].reduce, you can flatten an array of comparison functions into a single one.

const sortByMapped = map => compareFn => (a,b) => compareFn(map(a),map(b));
const flipComparison = fn => (a,b) => -fn(a,b);
const byValue = (a,b) => a - b;

const byPrice = sortByMapped(e => e.price)(byValue);
const byRating = sortByMapped(e => e.rating)(flipComparison(byValue));

const sortByFlattened = fns => (a,b) => 
  fns.reduce((acc, fn) => acc || fn(a,b), 0);

const byPriceRating = sortByFlattened([byPrice,byRating]);

const restaurants = [
  { name: "Foo's Burger Stand", price: 1, rating: 3 },
  { name: "The Tapas Bar", price: 3, rating: 4 },
  { name: "Baz Pizza", price: 3, rating: 2 },
  { name: "Amazing Deal", price: 1, rating: 5 },
  { name: "Overpriced", price: 5, rating: 1 }, 
];

console.log(restaurants.sort(byPriceRating));

/*
{name: "Amazing Deal", price: 1, rating: 5}
{name: "Foo's Burger Stand", price: 1, rating: 3}
{name: "The Tapas Bar", price: 3, rating: 4}
{name: "Baz Pizza", price: 3, rating: 2}
{name: "Overpriced", price: 5, rating: 1}
*/

Writing a random sort

You might want to sort an array "randomly." One technique that I have seen is to use the following function as the comparison function.

const byRandom = () => Math.random() - .5;

Since Math.random() returns a "random" number between 0 and 1, the byRandom function should return a positive number half of the time and a negative number the other half. This seems like it would be a good solution, but unfortunately, since the comparison function is not "consistent" — meaning it may not return the same value when called multiple times with the same values — it may result in some unexpected results.

For example, let's take an array of numbers between 0 and 4. If this byRandom function was truly random, it would be expected that the new index of each number would be spread out equally over enough iterations. The original 0 value would be just as likely to be in index 4 as index 0 in the new array. However, in practice, this function will bias each number to its original position.

See the Pen
Array.sort() Random 👎
by Adam Giese (@AdamGiese)
on CodePen.

The "diagonal" from the top-left will statistically have the greatest value. In an ideal and truly random sort, each table cell would hover around 20%.

The fix for this is to find a way to ensure that the comparison function remains consistent. One way to do this is to map the random value to each array element before the comparison, then map it away after.

const sortByMapped = map => compareFn => (a,b) => compareFn(map(a),map(b));
const values = [0,1,2,3,4,5,6,7,8,9];
const withRandom = (e) => ({ random: Math.random(), original: e });
const toOriginal = ({original}) => original;
const toRandom = ({random}) => random;
const byValue = (a,b) => a - b;
const byRandom = sortByMapped(toRandom)(byValue);

const shuffleArray = array => array
  .map(withRandom)
  .sort(byRandom)
  .map(toOriginal);

This ensures that each element has a single random value that is only calculated once per element rather than once per comparison. This removes the sorting bias towards the original position.

See the Pen
Array.sort() Random 👍
by Adam Giese (@AdamGiese)
on CodePen.

The post Level up your .sort game appeared first on CSS-Tricks.

​​Avoiding those dang cannot read property of undefined errors

​​​​Uncaught TypeError: Cannot read property 'foo' of undefined.​ The dreaded error we all hit at some point in JavaScript development. Could be an empty state from an API that returns differently than you expected. Could be something else. We don’t know because the error itself is so general and broad.

​​I recently had an issue where certain environment variables weren't being pulled in for one reason or another, causing all sorts of funkiness with that error staring me in the face. Whatever the cause, it can be a disastrous error if it’s left unaccounted for, so how can we prevent it in the first place?

​​Let’s figure it out.

​​Utility library

​​If you are already using a utility library in your project, there is a good chance that it includes a function for preventing this error. _.get​ in lodash​ (docs) or R.path in Ramda​ (docs) allow accessing the object safely.
​​
​​If you are already using a utility library, this is likely the simplest solution. If you are not using a utility library, read on!

​​

Short-circuiting with &&

​​​​One interesting fact about logical operators in JavaScript is that they don't always return a boolean. According to the spec, "the value produced by a &&​ or ||​ operator is not necessarily of type Boolean. The value produced will always be the value of one of the two operand expressions.”
​​
​​​​In the case of the &&​ operator, the first expression will be used if it a "falsy" value. Otherwise, the second expression will be used. This means that the expression 0 && 1​ will be evaluated as 0​ (a falsy value), and the expression 2 && 3​ will be evaluated as 3​. If multiple &&​ expressions are chained together, they will evaluate to either the first falsy value or the last value. For example, 1 && 2 && 3 && null && 4​ will evaluate to null​, and 1 && 2 && 3​ will evaluate to 3​.

​​​​How is this useful for safely accessing nested object properties? Logical operators in JavaScript will "short-circuit." In this case of &&​, this means that the expression will cease moving forward after it reaches its first falsy value.

​​​​

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

​​In this example, destroyAllHumans is never called because the &&​ operand stopped all evaluation after false​.

​​This can be used to safely access nested properties.

​​

​​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'

​​Aside from its simplicity, one of the main advantages of this approach is its brevity when dealing with small chains. However, when accessing deeper objects, this can be quite verbose.

​​

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

​​The more deeply nested an object is, the more unwieldy it gets.

​​
​​

The “Maybe Monad”

​​Oliver Steele came up with this method and goes through it in much more detail in his blog post, "Monads on the Cheap I: The Maybe Monad." I will attempt to give a brief explanation here.

​​

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

​​Similar to the short-circuit example above, this method works by checking if a value is falsy. If it is, it will attempt to access the next property on an empty object. In the example above, favorites.reading​ is null​, so the books​ property is being accessed from an empty object. This will result in an undefined​, so the 0​ will likewise be accessed from an empty array.

​​The advantage of this method over the &&​ method is that it avoids repetition of property names. On deeper objects, this can be quite a significant advantage. The primary disadvantage would be readability — it is not a common pattern, and may take a reader a moment to parse out how it is working.​

​​

​​try/catch

​​​​try...catch​ statements in JavaScript allow another method for safely accessing properties.

​​

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

​​Unfortunately, in JavaScript, try...catch​ statements are not expressions. They do not evaluate to a value as they do in some languages. This prevents a concise try​ statement as a way of setting a variable.

​​One option is to use a let​ variable that is defined in the block above the try...catch​.

​​

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

​​Although it’s verbose, this works for setting a single variable (that is, if the mutable variable doesn't scare you off). However, issues can arise if they’re done in bulk.

​​

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

​​If any of the attempts to access the property fails, this will cause all of them to fall back into their defaults.

​​An alternative is to wrap the try...catch​ in a reusable utility function.

​​

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"

​​By wrapping the access to the object in a function, you can delay the "unsafe" code and pass it into a try...catch​.

​​A major advantage of this method is how natural it is to access the property. As long as properties are wrapped in a function, they are safely accessed. A default value can also be specified in the case of a non-existent path.

​​Merge with a default object

​​
By merging an object with a similarly shaped object of "defaults," we can ensure that the path that we are trying to access is safe.
​​
​​

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"
​​  }
​​*/

​​
​​Careful, though, because the entire nested object can be overwritten rather than a single property.
​​
​​

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! To fix this, we'll need to similarly copy each of the nested objects.

​​

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

​​Much better!

​​This pattern is common with plugins or components that accept a large settings object with included defaults.

​​A bonus about this approach is that, by writing a default object, we’re including documentation on how an object should look. Unfortunately, depending on the size and shape of the data, the "merging" can be littered with copying each nested object.

​​​

The future: optional chaining

​​There is currently a TC39 proposal for a feature called "optional chaining." This new operator would look like this:

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

​​The ?.​ operator works by short-circuiting: if the left-hand side of the ?.​ operator evaluates to null​ or undefined​, the entire expression will evaluate to undefined​ and the right-hand side will remain unevaluated.

​​To have a custom default, we can use the ||​ operator in the case of an undefined.

​​

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

​​

Which method should you use?

​​The answer, as you might have guessed, is that age-old answer… "it depends." If the optional chaining operator has been added to the language and has the necessary browser support, it is likely the best option. If you are not from the future, however, there are more considerations to take into account. Are you using a utility library? How deeply nested is your object? Do you need to specify defaults? Different cases may warrant a different approach.

The post ​​Avoiding those dang cannot read property of undefined errors appeared first on CSS-Tricks.