JavaScript Practices That Will Help Your Teammates Sleep At Night
1. Avoid multiple files with the same name (especially with similar behavior)
Naming our files is seemingly a simple task. But just because it is simple to do doesn’t mean it wouldn’t become much of a problem. Just like naming our variables in code, it is a practice that can become a huge deal when you aren’t the only one writing the code for some project.
Recently, there was a colleague that maintained a package that I had to take over since he was no longer maintaining it.
In a perfect world all code should be easy to maintain and easy to work with Boy, not everything in life goes in our favor. One thing that confused me for awhile was coming across multiple files with the same name. What’s worse is that both of their implementation details were similar.
So I asked the questions,
-
- Which one has the function I need?
-
- Does changing one of these files mean I have to make a change in the other one as well?
-
- How can I merge them into one location if they share similar behavior?
The two files are named as:
-
- src/services/document.js
-
- src/utils/builtIn/document.js
The best practice here is not exactly putting them into one file. The bigger picture here is to follow the principle that our code should be easy to maintain. If we have to be confronted with making a risky decision on between two similar things, the code simply loses simplicity. What’s worse is the fact that services closely relates to utilities by their definitions.
2. Clean up dead stuff
It’s worth noting here that “stuff” can mean lots of things. The point is to get rid of dead/inactive code (including files) that is no longer of use in the project.
If we a have constants.js
file holding all the constants and then we have another constants.js
nested somewhere in another directory as the new location, it is a good practice to immediately remove the constant variables from the old file as soon as possible otherwise readers of your code yet again will suffer the same situation mentioned above.
If they end up importing from the wrong location in several parts of the code, someone has to go and fix each and every import before they produce errors in the production environment.
When we are debugging a function that is expecting arguments it is a good practice to name them appropriately that closely relates to the implementation details.
It is common to come across code that have their arguments written in function calls like this:
function createSignature(obj) {
if (typeof obj === 'string') {
// Do something
} else if (Array.isArray(obj)) {
// Do something
} else {
// Do something
}
}
There is nothing wrong with the code except the fact that it confuses everyone. People reading code like this will naturally expect that the parameter obj
will be some object. However, the implementation details contradict this.
Which one should we trust more, the implementation or the naming of the parameters?
We might as well not trust the entire function! A more generic naming like value
is even a better choice because a value can be any data type which is what the implementation shows.
So when we try to write functions that have different variations in its signature it is best to start off naming the parameters as generic as possible while still keeping a close relation to the body of the function and then work our way to more specific naming as we validate different data types:
function validateURL(value) {
if (typeof value === 'string') {
// URL string
} else if (value && typeof value === 'object') {
// URL instance
}
}
3. Unit tests
Make no mistake about it. Unit tests save time and sweat in the long run. Now I am not saying this because it is generally a recommended practice in every article out there. I am speaking from experience.
My experience developing in a project without unit tests compared to with was that developing and debugging code produced more sweat in a project with barely any unit tests. Unit tests produces the nice benefit of feeling confident moving forward.
This is what happens when you create unit tests:
You become a team player to your current and incoming team members. Establishing unit tests to code helps other teammates avoid pushing code that conflicts with current existing behavior. It
You get permanent protection throughout parts of your code. When you continue to develop code while falling way behind on unit tests and you make changes in the future, let me tell you, it is an absolute nightmare. What can happen is when you make changes that result in having to update another part of your code, there can be a devastating domino effect of errors propagating one after the other. This can commonly be the result of implicit dependencies in in our functions. The key word here is implicit. If you don’t take control of your code now, you will be missing out on crucial code and not realize it until you actually start receiving the annoying errors. You can establish unit tests now so that when you make changes to your code a couple months later, you still have control and confidence over your code. When you make a mistake, your unit tests will alert you immediately once you run them. This is the permanent protection I was referring to.
You get a firm architectural understanding on your code structure. In complex scenarios during runtime (when users are using your app) it is easy to miss exactly what went down when the user spams user actions, like clicking buttons multiple times, clicks forward, back, goes to their profile, visits their shopping cart, etc in seconds. Our eyes have limitations in what we see in real time (that is why we need debug tools to time travel backwards) as well as what we process mentally during those milliseconds of operations. Unit tests will help isolate the exact timing of your functions and you can test if they are behaving as expected in between function invocations.
4. Explicit TypeScript typings
This is one of those tips that sound obvious but needs to be said again and again. It is a wake up call to those who seldomly use TypeScript to write types that look like something like this:
type ReferenceString = string
type Obj = Record<string, any>
interface TransformFunc {
(
transformer: <V>(value: ReferenceString | Obj | any[]) => V,
...args: any
): any[]
}
const transform: TransformFunc = (transformer, ...args) => {
return args.reduce(
(arr, arg) =>
Array.isArray(arg)
? arr.concat(...arg.map((val) => transformer(val)))
: transformer(arg),
[],
)
}
On average you can get by coding like this for all your projects. But so can you with just this:
const transform = (transformer, ...args) => {
return args.reduce(
(arr, arg) =>
Array.isArray(arg)
? arr.concat(...arg.map((val) => transformer(val)))
: transformer(arg),
[],
)
}
The point is that TypeScript is not being pushed to its full potential and using it in this way just begs the question, “What’s the point of TypeScript?” When TypeScript is leveraged using more specific types and taking advantage of its features it offers, it really makes a big difference in the development flow.
One example to make more out of our code with TypeScript is being more explicit and declarative with our type alias ReferenceString
. If reference strings (lets just say they are placeholders for data values) are strings that are prefixed with a symbol, instead of declaring string
we can make TypeScript enforce that all strings that are being applied as this type are prefixed:
type ReferenceString<V extends string = string> = `.${V}`
Conclusion
And that concludes the end of this post! I have you found this to be valuable and look out for more in the future!