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โ
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.
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
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
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
} */
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:
- (value) => mutateValue(value, globalThing)
+ (value) => mutateValue(
+ (console.debug('[DEBUG] The Value:', value),
+ (console.debug('[DEBUG] Global thing:', globalThing)
+ ) // โจ Logs both `value` and `globalThing`, separately