But the truth, the unpleasant truth, is that these compact designs are often very useful. And they are, at the same time, quite simple. This means that anyone who is interested in the code in which they are used can master them and understand such code.
In this post, I'm going to take a look at some very useful (and sometimes cryptic) compact constructs that you might find in JavaScript and TypeScript. After studying them, you can use them yourself, or at least you can understand the code of those programmers who use them.
1. Operator ??
The operator for checking values for
null
and undefined
(nullish coalescing operator) looks like two question marks ( ??
). It is hard to believe that this, with such and such a name, is the most popular operator. True?
The meaning of this operator is that it returns the value of the right operand if the value of the left one is equal to
null
or undefined
. This is not quite clearly reflected in its name, but oh well, what is - that is. Here's how to use it:
function myFn(variable1, variable2) {
let var2 = variable2 ?? "default value"
return variable1 + var2
}
myFn("this has ", "no default value") // "this has no default value"
myFn("this has no ") // "this has no default value"
myFn("this has no ", 0) // "this has no 0"
Mechanisms are involved here that are very similar to those used to organize the operator's work
||
. If the left side of the expression is equal to null
or undefined
, then the right side of the expression will be returned. Otherwise, the left side will be returned. As a result, the operator is ??
great for use in situations where anything can be assigned to a variable, but you need to take some action in the event that null
or falls into this variable undefined
.
2. Operator ?? =
An operator used to assign a value to a variable only if it has a value
null
or undefined
(logical nullish assignment operator) looks like two question marks followed by an equal sign ( ??=
). Think of it as an extension of the above operator ??
.
Let's take a look at the previous code snippet, rewritten using
??=
.
function myFn(variable1, variable2) {
variable2 ??= "default value"
return variable1 + variable2
}
myFn("this has ", "no default value") // "this has no default value"
myFn("this has no ") // "this has no default value"
myFn("this has no ", 0) // "this has no 0"
The operator
??=
allows you to check the value of a function parameter variable2
. If it is equal to null
or undefined
, it will write a new value to it. Otherwise, the parameter value will not change.
Keep in mind that the design
??=
may seem incomprehensible to those who are not familiar with it. Therefore, if you are using it, you might want to add a short commentary with explanations at the appropriate place in the code.
3. Abbreviated declaration of TypeScript constructors
This feature is specific to TypeScript. Therefore, if you are a champion of JavaScript purity, then you are missing a lot. (Just kidding, of course, but this really does not apply to regular JS).
As you know, when declaring a class, they usually list all of its properties with access modifiers, and then, in the class constructor, assign values to these properties. But in cases where the constructor is very simple, and in it the values of the parameters passed to the constructor are simply written into the properties, you can use a construction that is more compact than the usual one.
This is how it looks:
// ...
class Person {
private first_name: string;
private last_name: string;
private age: number;
private is_married: boolean;
constructor(fname:string, lname:string, age:number, married:boolean) {
this.first_name = fname;
this.last_name = lname;
this.age = age;
this.is_married = married;
}
}
// , ...
class Person {
constructor( private first_name: string,
private last_name: string,
private age: number,
private is_married: boolean){}
}
Using the above approach when creating constructors definitely helps save time. Especially when it comes to a class that has many properties.
The main thing here is not to forget to add
{}
immediately after the description of the constructor, since this is a representation of the function body. After the compiler encounters such a description, it will understand everything and will do the rest itself. In fact, we are talking about the fact that both the first and second fragments of the TS code will eventually be transformed into the same JavaScript code.
4. Ternary operator
The ternary operator is a construct that is easy to read. This operator is often used instead of short instructions
if…else
, as it allows you to get rid of extra characters and turn a multi-line construct into a one-line construct.
// if…else
let isEven = ""
if(variable % 2 == 0) {
isEven = "yes"
} else {
isEven = "no"
}
//
let isEven = (variable % 2 == 0) ? "yes" : "no"
In the structure of a ternary operator, the first is a logical expression, the second is something like a command
return
that returns a value if the logical expression is true, and the third is also something like a command return
that returns a value if the logical expression is false. Although the ternary operator is best used on the right-hand sides of value assignments (as in the example), it can also be used autonomously, as a mechanism for calling functions when which function will be called, or with which arguments one and the same will be called. the same function is determined by the value of the logical expression. This is how it looks:
let variable = true;
(variable) ? console.log("It's TRUE") : console.log("It's FALSE")
Note that the structure of the statement looks the same as in the previous example. The disadvantage of using the ternary operator is that if in the future you need to expand one of its parts (either the one that refers to the true value of the logical expression, or the one that refers to its false value), this will mean that you need to go to the normal instruction
if…else
.
5. Using a short calculation cycle used by the operator ||
In JavaScript (and in TypeScript too), the logical OR operator (
||
) implements a shorthand computing model. That is, it returns the first expression evaluated as true
, and does not check the remaining expressions.
This means that if there is the following statement
if
, where the expression expression1
contains a false value (reducible to false
), and expression2
- true (reducible to true
), then only expression1
and will be calculated expression2
. Expressions espression3
and expression4
will not be evaluated.
if( expression1 || expression2 || expression3 || expression4)
We can take advantage of this opportunity outside the statement
if
, where we assign values to variables. This will allow, in particular, to write a default value to a variable in the event that some value, say, represented by a function parameter, turns out to be false (for example, equal undefined
):
function myFn(variable1, variable2) {
let var2 = variable2 || "default value"
return variable1 + var2
}
myFn("this has ", " no default value") // "this has no default value"
myFn("this has no ") // "this has no default value"
This example demonstrates how you can use an operator
||
to write to a variable either the value of the second parameter of a function or a default value. However, if you look closely at this example, you can see a small problem in it. The fact is that if there variable2
is a value in 0
or an empty string, then the var2
default value will be written to, since both and 0
and the empty string are converted to false
.
Therefore, if in your case you do not need to replace all false values with the default value, you can resort to a lesser known operator
??
.
6. Double bitwise operator ~
JavaScript developers are usually not particularly keen on using bitwise operators. Who cares about binary representations of numbers these days? But the fact is that because these operators work at the bit level, they perform the corresponding actions much faster than, for example, some methods.
If we talk about the bitwise operator NOT (
~
), then it takes a number, converts it to a 32-bit integer (discarding the "extra" bits) and inverts the bits of this number. This causes the value to be x
converted into value -(x+1)
. Why are we interested in such a conversion of numbers? And the fact that if you use it twice, it will give us the same result as a method call Math.floor
.
let x = 3.8
let y = ~x // x -(3 + 1), ,
let z = ~y // y ( -4) -(-4 + 1) - 3
// :
let flooredX = ~~x //
Notice the two icons
~
on the last line of the example. It may look strange, but if you have to convert a lot of floating point numbers to integers, this technique can be very useful.
7. Assigning values to object properties
The capabilities of the ES6 standard simplify the process of creating objects and, in particular, the process of assigning values to their properties. If property values are assigned based on variables that have the same names as these properties, then there is no need to repeat those names. Previously, it was necessary.
Here is an example written in TypeScript.
let name:string = "Fernando";
let age:number = 36;
let id:number = 1;
type User = {
name: string,
age: number,
id: number
}
//
let myUser: User = {
name: name,
age: age,
id: id
}
//
let myNewUser: User = {
name,
age,
id
}
As you can see, the new approach to assigning values to object properties allows you to write more compact and simple code. And such code is no more difficult to read than code written according to the old rules (which cannot be said about the code written using other compact constructs described in this article).
8. Implicit return of values from arrow functions
Did you know that single-line arrow functions return the results of calculations performed on their single line?
Using this mechanism allows you to get rid of an unnecessary expression
return
. This technique is often used in arrow functions passed to array methods such as filter
or map
. Here's a TypeScript example:
let myArr:number[] = [1,2,3,4,5,6,7,8,9,10]
// :
let oddNumbers:number[] = myArr.filter( (n:number) => {
return n % 2 == 0
})
let double:number[] = myArr.map( (n:number) => {
return n * 2;
})
// :
let oddNumbers2:number[] = myArr.filter( (n:number) => n % 2 == 0 )
let double2:number[] = myArr.map( (n:number) => n * 2 )
Applying this technique does not necessarily mean making the code more complex. Constructs like these are a good way to clean up your code a little and get rid of unnecessary spaces and extra lines. Of course, the disadvantage of this approach is that if the bodies of such short functions need to be extended, you will have to revert to using curly braces again.
The only peculiarity that will have to be taken into account here is that what is contained in the only line of the short arrow functions considered here must be an expression (that is, it must produce some result that can be returned from the function). Otherwise, such a design will be inoperative. For example, the above one-line functions cannot be written like this:
const m = _ => if(2) console.log("true") else console.log("false")
In the next section, we'll continue talking about single-line arrow functions, but now we'll talk about functions that cannot be created without curly braces.
9. Function parameters, which can have default values
ES6 introduced the ability to specify values that are assigned to default function parameters. Previously, JavaScript did not have such capabilities. Therefore, in situations where it was necessary to assign similar values to parameters, it was necessary to resort to something like a model of reduced operator calculations
||
.
But now the same problem can be solved very simply:
// 2
// ,
function myFunc(a, b, c = 2, d = "") {
// ...
}
Simple mechanism, right? But, in fact, everything is even more interesting than it seems at first glance. The point is that the default value can be anything - including a function call. This function will be called if the corresponding parameter is not passed to it when the function is called. This makes it easy to implement the required function parameter pattern:
const mandatory = _ => {
throw new Error("This parameter is mandatory, don't ignore it!")
}
function myFunc(a, b, c = 2, d = mandatory()) {
// ...
}
// !
myFunc(1,2,3,4)
//
myFunc(1,2,3)
Here, in fact, is the one-line arrow function that you can't do without curly braces when creating it. The point is that the function
mandatory
uses an instruction throw
. Pay attention - "instruction", not "expression". But, I suppose, this is not the highest price for the ability to equip functions with required parameters.
10. Casting any values to a boolean type using !!
This mechanism works on the same principle as the above construction
~~
. Namely, we are talking about the fact that to cast any value to a logical type, you can use two logical operators NOT ( !!
):
!!23 // TRUE
!!"" // FALSE
!!0 // FALSE
!!{} // TRUE
One operator
!
already solves most of this problem, that is, it converts the value to a boolean type, and then returns the opposite value. And the second operator !
takes what happened and simply returns the inverse of it. As a result, we get the original value converted to a boolean type.
This short construction can be useful in a variety of situations. First, when you need to ensure that a variable is assigned a real boolean value (for example, if we are talking about a TypeScript variable of type
boolean
). Second, when you need to perform a strict comparison (using ===
) of something with true
or false
.
11. Destructuring and spread syntax
The mechanisms mentioned in the title of this section can be talked about and talked about. The point is, if used correctly, they can lead to very interesting results. But here I will not go too deep. I will tell you how to use them to simplify the solution of some problems.
▍Destructuring objects
Have you ever faced the task of writing multiple values of object properties into ordinary variables? This task is quite common. For example, when it is necessary to work with these values (by modifying them, for example) and at the same time not to affect what is stored in the original object.
The use of object destructuring allows you to solve similar problems using the minimum amount of code:
const myObj = {
name: "Fernando",
age: 37,
country: "Spain"
}
// :
const name = myObj.name;
const age = myObj.age;
const country = myObj.country;
//
const {name, age, country} = myObj;
Anyone who has used TypeScript has seen this syntax in the instructions
import
. It allows you to import individual library methods without contaminating the project namespace with many unnecessary functions:
import { get } from 'lodash'
For example, this instruction allows you to import
lodash
only a method from the library get
. At the same time, other methods of this library do not fall into the namespace of the project. And there are a lot of them in it.
▍Spread syntax and creating new objects and arrays based on existing ones
Using the spread (
…
) syntax simplifies the task of creating new arrays and objects based on existing ones. Now this task can be solved by writing literally one line of code and without resorting to any special methods. Here's an example:
const arr1 = [1,2,3,4]
const arr2 = [5,6,7]
const finalArr = [...arr1, ...arr2] // [1,2,3,4,5,6,7]
const partialObj1 = {
name: "fernando"
}
const partialObj2 = {
age:37
}
const fullObj = { ...partialObj1, ...partialObj2 } // {name: "fernando", age: 37}
Note that using this approach to combining objects will overwrite their properties with the same name. This does not apply to arrays. In particular, if the arrays to be merged have the same values, they will all end up in the resulting array. If you need to get rid of repetitions, then you can resort to using a data structure
Set
.
▍Combining destructuring and spread syntax
Destructuring can be used in conjunction with the spread syntax. This allows you to achieve an interesting effect. For example, remove the first element of the array, and leave the rest untouched (as in the common example with the first and last element of the list, the implementation of which can be found in Python and other languages). And also, for example, you can even extract some properties from an object, and leave the rest untouched. Let's consider an example:
const myList = [1,2,3,4,5,6,7]
const myObj = {
name: "Fernando",
age: 37,
country: "Spain",
gender: "M"
}
const [head, ...tail] = myList
const {name, age, ...others} = myObj
console.log(head) //1
console.log(tail) //[2,3,4,5,6,7]
console.log(name) //Fernando
console.log(age) //37
console.log(others) //{country: "Spain", gender: "M"}
Note that the three dots used on the left side of the assignment statement must apply to the very last item. You cannot first use the spread syntax, and then describe individual variables:
const [...values, lastItem] = [1,2,3,4]
This code won't work.
Outcome
There are many more designs similar to the ones we talked about today. But, using them, it is worth remembering that the more compact the code, the more difficult it is to read it for someone who is not used to the abbreviated constructions used in it. And the purpose of using such constructs is not to minify the code or speed up it. This goal is only to remove unnecessary structures from the code and make life easier for the person who will read this code.
Therefore, to keep everyone happy, it is recommended that you maintain a healthy balance between compact constructs and regular readable code. It is always worth remembering that you are not the only person reading your code.
What compact constructs do you use in JavaScript and TypeScript code?