Skip to main content

Back to (enhanced) basics

ยท 12 min read
Krystian Sowiล„ski

Nowadays, no matter which framework we're using or if any at all, no matter if we're using TypeScript or not, we're still writing JavaScript (kinda). There are many helpful features in recent ECMAScript versions that may significantly improve both code readability and our efficiency. Let's jump into them!

String interpolationโ€‹

one of the core and most useful language features

Consider adding a label containing some math:

const a = 1
const b = 2

const label = 'Sum of ' + a + ' and ' + b + ' is ' + a + b
// ๐Ÿ’ฃ Sum of 1 and 2 is 12

This fails simply because both a and b are considered strings since we're concatenating along with string values.

We could fix it by wrapping math into parentheses:

const label = 'Sum of ' + a + ' and ' + b + ' is ' + (a + b)
// ๐Ÿ‘Œ Sum of 1 and 2 is 3

It works, so it's great, we can move on.

But is there a better way?

There is! And the answer is string interpolation. The above working solution could be simply replaced with a much more readable expression:

const label = `Sum of ${a} and ${b} is ${a + b}`
// โœจ Sum of 1 and 2 is 3

Ternary operatorโ€‹

Also called conditional operator. Its purpose is to simplify some if-statement-based code allowing us to achieve the same in a single expression.

Let's define a goal for the exercise: to check if a value has been provided or not, applying a fancy label.

function describe(value) {
if (!!value) {
return 'Value provided'
} else {
return 'Busted! There is missing value'
}
}

describe('something') // Value provided
describe() // โœจ Busted! There is missing value

It would be more complicated though when we would need to conditionally assign a value to a variable.

So how to get the label inline?

function describe(value) {
const label = !!value ? 'present' : 'missing'
return `Value is ${label}`
}

describe() // Value is missing
describe('something') // Value is present

Equality check - strict vs weakโ€‹

info

That's a quite important thing as it can reverse your code logic in some (if not most) cases.

There are two ways of checking equality of things: a strict and a weak one.

Weak comparison used to be a subject of JS memes.

JS non-intuitive double equals comparison

In short, it doesn't compare types and is discouraged most of the time. We will create a helper function to see how it behaves in different scenarios (including one that proves weak comparison to be useful, stay tuned!).

function checkEquality(source, testValue) {
const weakResult = source == testValue
const strictResult = source === testValue

return `
${source} == ${testValue} is ${weakResult}
${weakResult === strictResult ? 'and' : ', but'}
${source} === ${testValue} is ${strictResult}
`
}

Starting with the obvious one:

checkEquality('a', 'b')
// "a" == "b" is false and "a" === "b" is false

Nothing shocking yet, let's continue:

checkEquality([], 0)
// [] == 0 is true , but [] === 0 is false

Ok, let's make a pause. [] is an empty array, but we wouldn't expect it to be equal to 0, would we?

That's why weak comparison is discouraged. In fact, JS converts the values first in this case.

Think of the example as of an array that has 0 elements and simple 0 numeric value. The result would be the same for a blank string, even containing white spaces:

checkEquality(' ', 0)
// " " == 0 is true , but " " === 0 is false

Ok, so I think we can agree now that, by default, we should use strict comparison (===).

There's a useful case though - nullish values

note

Nullish value can be either null or undefined

Sometimes we don't mind if something is null or undefined as long as it's not e.g. an array, so we can push some elements into it, but only once it's defined.

With strict comparison we would be enforced to do similar things twice:

function getArray(value) {
if (value === null || value === undefined) {
return []
} else {
return value
}
}

This can be simplified though, thanks to weak comparison against nullish values:

function getArray(value) {
if (value == null) {
return []
} else {
return value
}
}


getArray() // []
getArray(["there's something!"]) // ["there's something!"]

Destructuring assignmentโ€‹

Have you ever found this challenging to get some properties of an object, while ignoring the rest? You surely have (or will soon ๐Ÿ˜‰). We need to do so when doing various sorts of data mapping, usually related to network calls.

Imagine a config object that contains a bunch of properties like:

const config = {
appName: 'The App',
env: 'DEV',
api: 'https://example.whapp.dev/api/v2',
oidc: {
// ...
},
}

We were told to create an application header including the name and environment.

Of course, we could make it using property access:

  const label = `${config.appName} (${config.env})`
// The App (DEV)

However, a destructed shape might be easier to use and more readable:

  const { appName, env } = config
const label = `${appName} (${env})`
// The App (DEV)

Spread operatorโ€‹

Array-likeโ€‹

Consider having two collections - for simplicity, arrays of numbers.

How could we e.g. merge those into single one? First thought - with loops, obviously! Let's give it a try.

function merge(a, b) {
const result = []
a.forEach((element) => {
result.push(element)
})
// and here we go again ๐Ÿ˜ฉ
b.forEach((element) => {
result.push(element)
})

return result
}

const arr1 = [1, 2, 3]
const arr2 = [7, 20]
merge(arr1, arr2) // [1, 2, 3, 7, 20]

Cool, it works. Though spread operator (...) is there to make this much simpler:

function merge(a, b) {
const result = [...a, ...b] // โœจ that's neat, isn't it?
return result
}

const arr1 = [1, 2, 3]
const arr2 = [7, 20]
merge(arr1, arr2) // [1, 2, 3, 7, 20]

It spreads all the elements in place.

Object propertiesโ€‹

Hold on, that's not the end of the story yet - we can spread object properties as well.

Imagine a shape:

const source = {
id: '1234abcd',
name: 'Here we are!',
metadata: {
something: 'irrelevant',
},
}

How to get most of the properties of an object but omit a few? Till the object is not huge it would be pretty much straightforward to get what we need with explicit mapping.

const source = {
id: '1234abcd',
name: 'Here we are!',
metadata: {
something: 'irrelevant',
},
}

const allButMetadata = {
id: source.id,
name: source.name
}

But how the code would look like if there were dozens of properties? It would be enormous spaghetti!

Here spread operator (...) comes in handy once again:

const {
metadata,
...allButMetadata
} = source

Nullish coalescing operatorโ€‹

One of the common challenges with handling data is that something is missing, and we want to apply a default value.

JS got a nice thing a while ago to handle that - we can just use someValue ?? defaultValue expression.

function describe(value) {
return value ?? 'I am not null anymore! ๐Ÿ’ช'
}

describe(null) // I am not null anymore! ๐Ÿ’ช
describe('Such a thing') // Such a thing
note

In fact there's also logical OR operator || of which special case is the nullish coalescing one.

The difference is that logical OR will evaluate to the default value for any falsy value, including empty strings and...0 number. That may be handy in some cases, though mostly we need to ensure that the value is not nullish.

function describe(value) {
return value || 'I was blank'
}

describe('') // I was blank
describe('Such a thing') // Such a thing

Nullish coalescing assignmentโ€‹

Pretty similar to nullish coalescing operator in its behavior but with a slightly different purpose. It helps in property/variable initialization when it's nullish, to stop being so.

function fulfill(source) {
source.js ??= 'rocks! ๐Ÿชจ'

return source
}

fulfill({
js: 'is nice'
})
/* {
js: 'is nice'
} */

fulfill({
thing: 'still here'
})
/* {
thing: 'still here',
js: 'rocks! ๐Ÿชจ'
} */

Lazy property assignmentโ€‹

Sometimes we need to assign a property to an object only in particular circumstances while having undefined or null as a value is not an option. We can consider using the spread operator:

// styles.js
const styles = {
box: {
border: '1px solid #000'
}
}
// index.js
const elementStyle = {
...styles.box, // โœ“ `box` was defined as an object
...styles.card, // ๐Ÿ’ฃ crashes, there's no `card` property at `styles`
}

So it doesn't work just like that. What else can we do? Using extra ternary operator is a common way to handle such issues.

const elementStyle = {
...styles.box, // โœ“ `box` was defined as an object
...(styles.card ? styles.card : {}),
// ๐Ÿ’ช doesn't crash anymore, even if there's no `card` property at `styles`
}

/* {
border: '1px solid #000'
} */

In fact, spread operator can take boolish values as well. Let's try this out:

const elementStyle = {
...styles.box, // โœ“ `box` was defined as an object
...(styles.card && styles.card),
// ๐Ÿ‘Œ doesn't crash anymore, even if there's no `card` property at `styles`
}

/* {
border: '1px solid #000'
} */

Cool, it's nice but there's a catch. Value wouldn't be assigned when falsy...

const disabled = false
const things = {
here: 'we go'
}

const result = {
...things,
...(disabled && {disabled}) // ๐Ÿ’ฃ `disabled` won't be assigned
}
/* {
here: 'we go'
} */

Here's a thing - nothing stops us to compare the value with undefined (or anything else):

const disabled = false
const things = {
here: 'we go'
}

const result = {
...things,
...(disabled !== undefined && {disabled}) // โœจ `disabled` will be assigned
}
/* {
here: 'we go',
disabled: false
} */
Did you know?

Within an object, there's a difference between property that has undefined value and a property that was not defined.

const someCollection = [1, 2, 3]
const source = {
foo: someCollection[4]
/*
`someCollection[4]` evaluates to `undefined` since
`someCollection` has only 3 elements
*/
}

source.foo // undefined
source.bar // undefined

Wait, I just said that these are different, right? Ok then...

Object.hasOwn(source, 'foo')
// true, we've defined `foo` to be `undefined`

Object.hasOwn(source, 'bar')
// false, there's no `bar` property in source

Optional chainingโ€‹

I think we can agree that we're dealing with nullish values pretty often. Usually we're trying to access a property on an object that doesn't really exist.

Sometimes we're reaching top level property to test against nullish values. In such cases we could just write a simple if-statement and exit early. But things used to be much more complicated than that.

Let's say we have a blob storage of documents in the cloud and many users have access to edit it while the first user is the initial creator. We want to display information about the second contributor somewhere.

In the ideal world everything would be easy, and we'd have an accessor like this one:

const contributorName = file.metadata.contributors[1].fullName
// ๐Ÿ’ฃ TypeError: undefined is not an object โ˜๏ธ

Unfortunately, following contributors are not returned separately, but in a joint collection.

We've got a requirement from the Product Owner to display - in case there's only one contributor.

const contributorName = file.metadata.contributors[1]?.fullName ?? '-'
// โœจ One character that makes the difference โ˜๏ธ

It's pretty neat when used along with nullish coalescing operator

in operatorโ€‹

Sometimes we need to check if there's a particular property in an object.

Assume we're dealing with an object that has either stringValue or numberValue, never both. What's worse, these values could be undefined or null.

How to tell which case we're dealing with?

By checking if such object has a particular property:

const source = {
numberValue: undefined
}
if (`stringValue` in source) {
/* `source.stringValue` is there,
but we don't know/mind if it's non-blank string or nullish value
*/
} else {
/* `source.stringValue` isn't there,
so we may assume we need to use `source.numberValue`
*/
}

Comma expressionโ€‹

Last but not least. It's rarely used in repositories, although it could be present in local code that has not been pushed yet.

Comma expressions does everything from left to right but returns only the last part.

It's like in a CSV file we would return the last column.

let x = 1;

/* these are executed but not assigned to `y`
๐Ÿ‘‡ ๐Ÿ‘‡ */
const y = (++x, x++, ++x);
/* โ˜๏ธ
* `y` will be 4
*/

That probably wasn't the best use case, but keeping in mind how it behaves, you could easily debug things inline:

spaghetti-code.js
- (value) => mutateValue(value, globalThing)
+ (value) => mutateValue(
+ (console.debug('[DEBUG] The Value:', value),
+ (console.debug('[DEBUG] Global thing:', globalThing)
+ ) // โœจ Logs both `value` and `globalThing`, separately