This is my set of a few good practice guidelines for JS/TS. In my experience, applying these rules will make the code a lot easier to maintain.
Table of contents
{- General rules
- General code style rules
- Always prefer const
- Exports
- Arrow functions
- Null checks
- Looping through arrays
- Spread operator
- Conditions
- Typescript
}
🠹 General rules
This applies to any language not just JS!
- Avoid using libraries when possible, especially if you can replicate the methods easily (lodash users I'm talking to you). Reasons? Over-sizing your app, and adding code you don't know.
- Always focus on readability rather than "looking smart". Avoid using shorthands or hacks, or using single-character named variables. At the end of the day, the language is not what will make your app slow. If you need to optimize your app, try to optimize images and http requests.
- Prevent duplicated code. You can create any number of reusable functions and it will be far better than re-writing code. Why? If you have to refactor later, you will have to find every copy of the logic while you could just update the function instead.
- Keep functions small. Your function should not work for more than 1 use case. Avoid using a lot of if/else that changes the way it works, create several functions instead. The only place where you could change the implementation based on conditions is within your controllers. In that case, you would call the correct function based on your desired condition.
🠹 General style rules
- Always use
;
at the end of each line - Always include spaces between brackets:
import { Thing, OtherThing } from './stuff';
- Always declare arrays in single line when it has only a few elements:
[ 'thing', 'thing2' ];
- Keep your code easy to understand, if you're using ternary operators, use it only for one condition - do not chain them
- No matter what kind of indentation you use (spaces/tabs) as long as you use width of 4, this keeps the code clean and very easy to read
- Use spaces after each parameter in a function or method and between different parts of the declaration:
function something(param1, param2, param3) {
- Use spaces to keep conditions and loops easy to read:
if (condition) {
/for (let i = 0; i < 5; i++) {
🠹 Always prefer const
I prefer this for easy debugging and preventing errors. Remember, const
means you cannot re-assign that variable, but you can change it's value if it is an array or an object.
const rows = [];
// This is a React example, but it's valid for other cases too
people.forEach(person => {
rows.push(
<Row>
<Col>{person.name}</Col>
</Row>
);
});
const settings = {};
settings.darkMode = true;
🠹 Exports
If you have a module with the main method being something like lowercase
, export only that method.
function internal(str) {
return str.toLowerCase();
}
function lowercase(str) {
return internal(str);
}
// Do not export everything
module.exports = { internal, lowercase };
// Instead, export the useful methods
module.exports = { lowercase };
// This is a default export
module.exports = function() {}
// This is a named export
function something() {}
module.exports = { something };
// Example for default export usage
// Both will work and they will have different name while they are the same method
const something = require('./something');
const notSomething = require('./something');
// This will make the method use the same name across your project (unless changing it on purpose)
// Plus instead of importing the whole module, you just import the method you need
const { something } = require('./something');
🠹 Arrow functions?
I know, arrow functions are sexy. However, they are not always needed. Considerate using them only when:
- You need to maintain context for
this
- You only have a single-line logic, like when using
.map()
or.filter()
This will make also the previous rule easier to keep.
🠹 Null checks
Always add nullchecks to prevent errors in your console and breaking the flow.
You can use the falsy check or defaulting some value with the ||
operator. For example:
const name = user.name || 'Unavailable';
const name = user?.details?.name || 'Unavailable';
🠹 Looping through arrays
const adults = people.filter(person => person.age >= 18);
Use .forEach()
if you don't need to create a new array
people.forEach(person => registerPerson(person));
In this case, you can use .map()
since you need a new array with elements changed
// This way you add new properties to array
const people = peopleArray.map(person => ({
...person,
isAdult: (person.age >= 18)
});
If you know what items you have in an array and want to use them, for instance the result of a .split()
operation, you can do something like this:
// Instead of
const parts = fullname.split(' ');
const name = parts[0];
const lastname = parts[1];
// You can do
const [ name, lastname ] = fullname.split(' ');
🠹 Spread operator
I know sometimes it looks fancy but if you need to grab 8 properties from a 10 properties object, then it's more convenient to just use the object instead of spreading each property from it:
// Instead of doing
const { prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8, prop9, prop10, prop11, prop12 } = obj;
// Refence the object itself
<p>Name: {obj.prop1}</p>
<p>Lastname: {obj.prop2}</p>
Your code will look cleaner with a lot less lines while getting the same result.
🠹 Conditions
If you have a condition that always returns either true
or false
, you don't need to compare it:
// Do not use
if (something == true) {
// Instead just do
if (something) {
// Do not use
result = (condition != "") ? true : false;
// Just do this
result = (condition != "");
// Do not do this
function isSomethingTrue(something) {
if (something) {
return true;
} else {
return false;
}
}
// Instead do this
function isSomethingTrue(something) {
return (something);
}
Sounds weird, but trust me your code will start to look a lot cleaner and easier to understand.
You can use return
or throw
to stop the flow.
There's a good article right here
function getName() {
if (!person.name) {
return 'Name not available';
}
return person.name;
}
🠹 Typescript
"The whole point of using JS is that I don't have to care about types". I know, most JS developers when first presented TS hate it for this reason. I can assure you, it will help you prevent a lot of common errors. There's no need to use it but if you do, here's some useful tips.
Types are not only useful to prevent errors and fail compilation if there's a typo somewhere. They're also extremely useful for new developers. You can define the interface of your API (and other things) and welcome new developers to your project.
interface CreateUserRequest {
name: string;
lastname: string;
age?: number;
}
// This way people just need to check CreateUserRequest interface to find out what properties you accept
// Also if the IDE supports it, they will get autocomplete to make the DX delightful.
export function CreateUser(body: CreateUserRequest) {
Let's say your app depends on a service that returns a huge object and you only need a few properties. You don't need to type every property in your project, instead, you can use an interface with the properties that you know and use.
You don't need to rewrite types if you only have to discard one of two properties. Imagine you have a REST API and you have a product
entity. When you create an object for that entity, you don't know the Id, but when you make a GET request you do. In that case you can do something like this:
export type Product {
name: string;
price: number;
id: string;
}
// This is the same as Product, without the id
export type ProductCreate = Omit<Product, 'id'>;
Most TS developers don't take full advantage of the features available. There are a lot of places where any
should not be an option. Usually they use any
when they want to reuse a method across different entities. But you don't need to do that!
// ResponseType is dynamic and you can send TS what type you are expecting from the method
export function Request(userId: number): Response<ResponseType> {
return { name: 'Alan' } as ResponseType;
}
// You use it like this:
type User = { name: string };
// response will be of type User and the IDE will get full autocomplete options too
const response = Request<User>(1);
If you have a React/Node app and a monorepo, you can share your types between both parts of the app. This way, you only have to update the types in one place and is easier to maintain.