10 JavaScript Practices You Should Know Before Tomorrow

Christopher T.

February 21st, 2022

Share This Post

I've had multiple junior developers and interns join the team over the years and I found one thing that they all have in common (most of the time): They end up unnecessarily writing longer ways to do something in code when there are shorter (as well as performant) ways to do the same thing.

With that said this post will mostly be aimed for JavaScript developers who are learning the language as well as those who are just curious to potentially find out what's more out there in JavaScript. There are some tips here that I didn't know about until the later stages of my development career.

Without further ado, here are 10 JavaScript Practices You Should Know Before Tomorrow:

Filtering Arrays To Unique

ES6 introduced features that influenced the majority of our code base today. There is the spread operator as well as the Set object. These two can be combined to a one liner that filters an array of primitive values to be fully unique:

const symb1 = Symbol('hello')
const symb2 = Symbol('hello')

// prettier-ignore
const arr = [1, '1',1, 3, 'morning', null, '', 3, 121,true,symb1,symb1, 121, true, symb1, symb2]
const uniqueArr = [...new Set(arr)]

Result:

;[1, '1', 3, 'morning', null, '', 121, true, Symbol(hello), Symbol(hello)]

Note: This JavaScript trick is the cleanest solution for arrays containing only primitive data types such as boolean, string, undefined, null, number. For the Symbol type, each constructed Symbol is unique so Symbol(hello) and Symbol(hello) are actually two different instances (declared as variable symb1 and symb2)

Converting to Boolean

JavaScript treats all values as truthy or falsey except for the true and false values themselves. This is intended by the semantics of the language:

if ('true') {
  // Truthy
}
if ('false') {
  // Truthy
}
if (null) {
  // Falsy
}
if (0) {
  // Falsy
} else if (function () {}) {
  // Truthy
}
if (new Array().fill(null)) {
  // Truthy
}
if (Object.create(null)) {
  // Truthy
}
if (1) {
  // Truthy
}

All values in JavaScript are truthy except (when otherwise specified):, false, '', null, NaN, undefined, and 0 which JavaScript interprets as all falsy.

The most concise way to convert values to boolean (true or false) is by negating values with ! as shown below:

const isTrue = !0
const isFalse = !1
const alsoFalse = !!0
console.log(isTrue) // Result: true
console.log(typeof true) // Result: "boolean"

This type of coercion can become really useful when handling functions that take multiple input especially when you're looking to create simple and smaller functions:

Also, with 0 and 1 we can also convert them to true and false just as easily:

console.log(+true) // Result: 1
console.log(+false) // Result: 0

true is the equivalent to 1 and false is the equivalent of 0:

console.log(-true) // Result: -1
console.log(-false) // Result: -0

Converting values to strings

We can quickly convert values to strings by a simple + operator immediately following quotes:

const val = 1 + ''
console.log(val) // Result: "1"
console.log(typeof val) // Result: "string"

Converting values to numbers

Similarly to the previous example, we can also convert strings to numbers with the same + operator by prefixing strings with it:

let age = '80'
age = +age
// Result: 80

We can use this to increment numbers by true or false as well (remember that true is equivalent to 1 and false as 0):

let age = 30
age = age + true
// Result: 31

age = 30
age = age + false
// Result: 30

Compound Assignment (short circuiting) operators

Logical And (&&)

Instead of this:

const john = { id: null, age: 30, email: 'john@gmail.com' }
const mike = { id: '456', age: 31, email: 'mike@gmail.com' }
const sally = { id: '789', age: 32, email: 'sally@gmail.com' }
const kelly = { id: null, age: 25, email: 'kelly@gmail.com' }
const george = { id: '131415', age: 40, email: 'george@gmail.com' }

const getProfileOfAgeWithSubmittedEmail = (minAge, ...profiles) => {
  for (const profile of profiles) {
    let age

    if (profile.email && profile.age) {
      age = profile.age
    }

    if (typeof age === 'number' && age >= minAge) {
      return profile
    }
  }
}

// prettier-ignore
const profile = getProfileOfAgeWithSubmittedEmail(32, john, mike, sally, kelly, george)
// Result: sally

We can use the logical and (&&) operator:

const getProfileOfAgeWithSubmittedEmail = (minAge, ...profiles) => {
  for (const profile of profiles) {
    const age = profile.email && profile.age
    if (typeof age === 'number' && age >= minAge) {
      return profile
    }
  }
}

// prettier-ignore
const profile = getProfileOfAgeWithSubmittedEmail(32, john, mike, sally, kelly, george)
// Result: sally

This does three separate things in just one line:

  1. Evaluate profile.email
  2. If the result (profile.email) is falsy, then return profile.email
  3. Otherwise if the result is truthy, then evaluate as well as return profile.age

It is shorter but also when you use it a lot in your code you will start to realize you are naturally writing const instead of let (or var) more, which is a best practice if you expect assigned values will never be changing after evaluating which is key in writing cleaner code.

Logical Or (||)

Similarly to the above, instead of this:

const getProfileOfAgeWithMissingId = (maxAge, ...profiles) => {
  for (const profile of profiles) {
    let id

    if (profile.age < maxAge) {
      id = profile.id
    }

    if (!id) return profile
  }
}

// prettier-ignore
const profile = getProfileOfAgeWithMissingId(30, john, mike, sally, kelly, george)
// Result: kelly

We can use the logical and (||) operator as a shorter alternative:

const getProfileOfAgeWithMissingId = (maxAge, ...profiles) => {
  for (const profile of profiles) {
    const id = profile.age < maxAge || profile.id
    if (!id) return profile
  }
}

// prettier-ignore
const profile = getProfileOfAgeWithMissingId(30, john, mike, sally, kelly, george)
// Result: kelly

This does three separate things in just one line:

  1. Evaluate profile.age < maxAge
  2. If the result is falsy, then return false
  3. Otherwise if the result is truthy, then evaluate as well as return the result of profile.id

The advantage of short circuiting this way is that you can hit two conditons (#2 and #3) in one line of code (profile.age < maxAge || profile.id) which is great!

Logical Assignment Operators (Introduced in ES2021)

Instead of this:

const john = { id: null, age: 30, email: 'john@gmail.com' }
const mike = { id: '456', age: 31, email: 'mike@gmail.com' }

let person1
let person2

person1 = person1 || (person1 = john)

console.log(person1)
// Result: { id: null, age: 30, email: 'john@gmail.com' }

We can instead do this:

const john = { id: null, age: 30, email: 'john@gmail.com' }
const mike = { id: '456', age: 31, email: 'mike@gmail.com' }

let person1
let person2

person1 ||= john

console.log(person1)
// Result: { id: null, age: 30, email: 'john@gmail.com' }

There is hardly a difference in syntax but the benefit of person1 ||= john is short circuiting where the assignment of john is only evaluated if person is falsy.

Other equivalents to the short circuiting logical assignment operator are:

person1 &&= john // If person1 is truthy then assign john to person1
person1 ??= john // If person1 is null or undefined then assign john to person1

Customizing Property Keys

An interesting trick is to customize how objects coerce to strings when assigned to another object as a property to that object.

Normally when we assign objects as properties we get '[object Object]':

const john = {
  id: null,
  age: 30,
  email: 'john@gmail.com',
}

const obj = {}

obj[john] = john

console.log(obj)

result:

{ "[object Object]": { "id": null, "age": 30, "email": "john@gmail.com" } }

This is not very ideal because this is how it would turn out if we wanted to use JSON.stringify to save data to some data source:

const output = JSON.stringify(obj, null, 2)
console.log(output)

// Result:
// { "[object Object]": { "id": null, "age": 30, "email": "john@gmail.com" } }

Since all objects by default coerce to [object Object] and objects can only keep 1 unique key in an object at a time we also lose other data when we want to use them as keys (converting Map objects to their JSON representation when they can hold JavaScript objects for example).

To solve this issue we can define the toString() method and return the value we want as the key when assigned as a property on objects:

const john = {
  id: null,
  age: 30,
  email: 'john@gmail.com',
  toString: () => `John_${john.age}_${john.email}`,
}

const obj = { [john]: john }
// Or -->   obj[john] = john

console.log(obj)

Output:

{
  "John_30_john@gmail.com": {
    "id": null,
    "age": 30,
    "email": "john@gmail.com"
  }
}

Now we can even create Map instances and easily serialize them into a format we want:

const createPerson = (options) => {
  const person = {}
  for (const [key, value] of Object.entries(options.props)) {
    person[key] = value
  }
  if (typeof options.key === 'function') {
    Object.defineProperty(person, 'toString', {
      value: function getPropertyKey() {
        return options.key(person)
      },
    })
  }
  return person
}

const john = createPerson({
  props: { id: null, email: 'john@gmail.com' },
  key: (values) => {
    return `John_${values.age}_${values.email}`
  },
})

const mike = createPerson({
  props: { id: '456', age: 31, email: 'mike@gmail.com' },
  key: (values) => `Mike_${values.email}`,
})

const map = new Map()

map.set(john, john)
map.set(mike, mike)

map.toJSON = function () {
  const output = Object.assign(
    {},
    [...this.entries()].reduce((acc, [key, value]) => {
      acc[key] = value
      return acc
    }, {}),
  )

  return output
}

john.age = 30

console.log(JSON.stringify(map, null, 2))

Result:

{
  "John_30_john@gmail.com": {
    "id": null,
    "email": "john@gmail.com",
    "age": 30
  },
  "Mike_mike@gmail.com": {
    "id": "456",
    "age": 31,
    "email": "mike@gmail.com"
  }
}

Customizing Coerced Property String Tags

In the previous example we learned how to customize the JSON stringified output of objects. But we can also customize the tag part of the coerced [object Object] value:

function Warrior() {
  Object.defineProperty(this, Symbol.toStringTag, {
    value: 'Warrior',
  })
  this.hp = 100
}

const sally = new Warrior()

console.log(sally.toString()) // Result: [object Warrior]

We can use this[Symbol.toStringTag] = 'Warrior', but it can show up in the console when we log our instance:

Warrior { hp: 100, [Symbol(Symbol.toStringTag)]: 'Warrior' }

By using Object.defineProperty we prevent it from being displayed to the console. (We can configure that with the enumerable property)

function Warrior() {
  Object.defineProperty(this, Symbol.toStringTag, {
    enumerable: true, // Include it in log output if true. Defaults to false
    value: 'Warrior',
  })
  this.hp = 100
}

A good use case for this is to have some helper function that easily checks if an object is an instance of something like our Warrior:

function isWarrior(value) {
  return String(value) === '[object Warrior]'
}

const sally = new Warrior()

console.log(isWarrior(sally)) // Result: true

Creating Partial Functions With Prepended Arguments

We can create functions that take prepended arguments which can be called later with the prepended arguments in addition to new arguments.

For example:

function isOlder(person, targetPerson) {
  return targetPerson.age > person.age
}

console.log(isOlder(john, mike))
// Result: true

We can easily make a new re-usable function that compares the result on a prepended value:

const isOlderThanJohn = isOlder.bind(null, john)

console.log(isOlderThanJohn(mike))
// Result: true

console.log(isOlderThanJohn({ age: 11 }))
// Result: false

Retrieving the Last Item in Arrays

One way we can retrieve the last item in arrays is the more commonly used:

const people = [john, mike, kelly, sally]
const lastPerson = people[people.length - 1]

// Result --> sally

A trick we can use is using the .slice method on arrays. Since JavaScript can take negative integers in .slice it will begin taking values at the end of the array which can quickly be used to grab the last item if we start from -1:

const people = [john, mike, kelly, sally]
const lastPerson = people.slice(-1)

// Result --> sally

Conclusion

And that concludes the end of this post! I hope you found this to be valuable and look out for more in the future!


Top online courses in Web Development

Tags


Read every story from jsmanifest (and thousands of other writers on medium)

Your membership fee directly supports jsmanifest and other writers you read. You'll also get full access to every story on Medium.

Subscribe to the Newsletter

Get continuous updates

Mediumdev.toTwitterGitHubrss

© jsmanifest 2023