Adding Type Safety to Your JavaScript: An Overview to Better Code
JavaScript has long been a favorite among programmers and developers, and for good reason. Its versatility and manageable entry-level make it a top choice for a wide range of projects, including:
- Backend (NodeJS),
- Web apps (React, Angular, Vue),
- Mobile applications (React Native),
- Desktop applications (Electron),
- Machine learning (TensorFlow.js, ml5.js).
But as great as JavaScript is, it does have its drawbacks. Being a weakly typed language, it can sometimes lead to unexpected bugs and issues. In this article, we’ll explore the benefits of adding type-checking to your JavaScript code and how to avoid the common pitfalls of weakly typed languages. From my own experience, I’ll share a story of a project plagued by bugs due to data discrepancies and a lack of type safety. I’ll also show you how you can use simple tools and techniques to make your code more resilient and reliable.
A quick note: I won’t be showing you how to configure the tools I’ll be mentioning in this article, and all examples were made using VS Code. Your experience may vary. Additionally, I won’t consider TypeScript in this article since it’s strictly about JavaScript projects.
Keep reading if you’re ready to take your JavaScript skills to the next level and avoid the common headaches of weakly typed languages.
Types of type checking
Runtime
Runtime type checking occurs during the execution of the code. An example in React is PropTypes, which verifies, at runtime, whether the received props are of the correct type. However, this method is less helpful as it requires waiting for the app to reload after each change, navigating through a correct flow, and is prone to errors. It’s easy to miss a type-related bug or an edge case using this approach. Due to these limitations, no runtime tools will be included in this article.
Build-time
Build-time type checking takes place during the build phase of the code. Tools following this approach can be integrated into your build pipeline, preventing the build from succeeding if there are type errors. An example of this is Flow.
Static Analysis Time
Type checking during static analysis happens while writing code, without actually building or running it. This proactive approach allows developers to catch type-related issues early in the development process, minimizing the chances of errors slipping through undetected.
Enabling Type Checking in JavaScript
Well, you can’t. But you can. Sort of.
Flow
Citing after their official site: “Flow is a static type checker that allows developers to check for type errors while developing code.
Flow is designed to find type errors at compilation time, avoiding type errors at runtime. Flow works by using annotations in special JavaScript comments to indicate the expected type for an object or variable.”
Flow is an external library that hooks itself into your build pipeline to provide type safety. However, with a little bit of external tooling and configuration, you can hook up Flow to your IDE to provide real-time cues.
The actual notation is very-typescript-like. Let’s take a look.
// @flow
interface Person {
firstName: string;
lastName: string;
birthYear: number;
setBirthYear: (birthYear: number) => void;
middleName?: string;
}
function getFullName(firstName: string, lastName: string): string {
return `${firstName} ${lastName}`;
}
function getFullName(person: Person) {
return `${person.firstName} ${person.middleName} ${person.lastName}`;
}Flow’s webpage nicely documents all the notation and features available.
When configured properly you can enjoy working code completion…
…and real-time static code analysis.
Pros:
- It can be integrated into your build, thus it can catch code with type errors.
- Notation highly resembles TypeScript
Cons:
- It can take a bit of effort to configure the tool correctly in both the IDE and your builds.
- The integration with certain projects can be spotty.
JSDoc
What is JSDoc?
“JSDoc 3 is an API documentation generator for JavaScript, similar to Javadoc or phpDocumentor. […] JSDoc’s purpose is to document the API of your JavaScript application or library.”
However, it’ll do just fine with almost any type of application. I used JSDoc to document and apply type checking in the React application mentioned in the intro.
All major IDEs support JSDoc. The good part is it doesn’t require any additional tooling.
Let’s take a look at a code example:
/**
* Returns full name by combining first name and last name
* @param {string} firstName
* @param {string} lastName
* @return {string} Full name
*/
function getFullName(firstName, lastName) {
return `${firstName} ${lastName}`;
}/**
* A person
* @typedef {Object} Person
* @property {string} firstName - First name
* @property {string} lastName - Last name
* @property {number} birthYear - Birth year
* @property {(birthYear: number) => void} setBirthYear - Set person's birth year
* @property {string} [middleName] - Optional middle name
*/Let’s take a look at the same function but that’s taking an object with structure defined as above.
/**
* Returns full name of a person
* @param {Person} person
* @return {string} Full name
*/
function getFullName(person) {
return `${person.firstName} ${person.middleName} ${person.lastName}`;
}Don’t worry if you feel a little lost, here you can find a handy cheat sheet of JSDoc notation, and for a full reference please go here.
Without JSDoc, we’re unable to determine with full certainty what type of data the getFullName function is expecting. It’s not the worst-case scenario with the first example; it’s possible to infer that from reading the code. JavaScript is also a resilient language, so it would most likely handle an undefined value if passed.
However, with more complex code, it quickly becomes tricky and invites inconsistencies that will only grow over time. Take the second getFullName function: without JSDoc, we only get the ominous any type, as shown below.
When the IDE can’t help us, we make ourselves prone to mistakes. With JSDoc, we would know that the person parameter is an object.
But it can get even better. By including the // @ts-check comment at the top of the file (or by enabling “js/ts.implicitProjectConfig.checkJs”: true in jsconfig.json if you want to enable it for the whole project), the TypeScript engine can also start looking for type errors and utilize full static code analysis.
That’s useful and safe. Make sure you read till the end; I’ll show you how to supercharge JSDoc methods with a bit more of TypeScript.
Pros:
- It provides type safety and code completion without the need for additional tooling.
- It doesn’t interfere with project configuration and is project-type agnostic.
- It doesn’t influence build times.
Cons:
- JSDoc is comment-based. It won’t stop a build or publishing code with errors, thus allowing runtime errors.
- Notation might get lengthy.
Bonus: Supercharge JSDoc with TypeScript interfaces
Did you know that JSDoc works nicely together with TypeScript? We can use that to provide type declarations in our JavaScript files. How? Let’s consider the following project structure:
my-app/
├─ Person.ts
├─ person.jsPerson.ts holds an interface:
export interface Person {
firstName: string;
lastName: string;
birthYear: number;
setBirthYear: (birthYear: number) => void;
middleName?: string;
}In person.js, we only import the interface inside JSDoc to get all the type safety goodies.
Neat, huh?
Summary
We explored the benefits of integrating type-checking tools like Flow and JSDoc into JavaScript projects. These tools improve code reliability, catch errors early, and provide type safety.
Despite potential challenges, the advantages of improved code completion and project maintainability make these tools invaluable for developers seeking to improve their JavaScript projects.
Let me know in the comments if you use any of the tools mentioned in your projects ;)
