Table of Contents
Before TypeScript Tips
Before showing you these TypeScript tips, let’s give a little context about the language.
JavaScript is the most in-demand programming language in the market and one of the most beloved by the developer community worldwide.
It is a highly flexible and easy-to-learn language, among other things, thanks to its dynamic typing, which automatically assigns a type to variables based on their values at each moment of execution.
The cost of developing software without typing is that all compatibility errors that could be detected while writing the code are allowed, leading to runtime errors that you’ll only discover once they occur. So, most of the time you save due to dynamic typing ends up being used to find and fix the bugs that will emerge.
How could we take advantage of JavaScript and avoid its error-proneness?
By using TypeScript. It’s a typing layer for JavaScript developed by Microsoft that captures compatibility errors in variables while we write the code, rather than waiting for someone to discover the flaw, report it, and then fix it.
I’m Carlos Sala, a software developer. In this article I’m not going to explain TypeScript from scratch because for this you can read the official documentation written by Microsoft. But I am going to give you 7 TIPS to use TypeScript like a pro and take advantage of all its possibilities.
#1 – Use TypeScript ESLint
Have you ever used ESLint in a JavaScript project?
ESLint is a tool that we can install through the NPM or Yarn dependency manager, and it analyzes the code in all files to identify potential issues during execution and maintain a consistent code style throughout the project. For me, it’s a crucial tool for optimizing code readability and maintenance.
The TypeScript ESLint plugin extends ESLint’s capabilities to analyze TypeScript syntax and enforce language best practices when writing and formatting code.
The TypeScript ESLint rules are fully customizable, allowing us to start with the recommended configuration and make modifications to tailor it to our project and personal preferences. To provide some examples of available rules:
- Disallow the declaration of empty interfaces.
- Disallow the explicit use of the
any
type. - Or not explicitly specify the type of simple data that can be inferred from a variable with assignment.
To seal the use of TypeScript ESLint, what I always do to enforce following these rules is to install the ESLint extension in Visual Studio Code or the IDE I’m using. This way, I receive real-time ESLint analysis without having to run any commands in the terminal.
#2 – TypeScript Strict Configuration
To ensure that TypeScript does its job of capturing all typing errors during development, we must configure it appropriately in our project.
In the tsconfig.json file, within the compileOptions
section, we must mark the strict
option as true
to activate strict typing.
{
"compilerOptions": {
"strict": true
}
}
This flag globally activates a series of options related to type checking. Some of them are:
- noImplicitAny: Forces us to add types to all variable declarations explicitly.
- strictNullChecks: Requires us to check if a variable is different from
null
orundefined
before using it. For example, after using thefind
method on an array, we must verify if a result has indeed been found; otherwise, we’ll encounter the error “object is possibly undefined.” - strictFunctionTypes: Strictly checks that the types of function parameters are correct.
As I said at the beginning, if we are using TypeScript it is to avoid scares during the execution of our application, not to show off that we are using it.
So, having a more flexible TypeScript configuration is self-defeating if we program without being careful, only to later encounter all the errors we’ve introduced.
#3 – Use TypeScript Generics
Do you use TypeScript Generics?
Similar to Java Generics or C++ Templates, in TypeScript, we can write code that is not tied to any specific type but rather to a type passed as a parameter. This allows us to create reusable code and apply it to different data types.
We can use Generics in types, functions, or classes to avoid duplicating code that, except for one or more types, it behaves in the same way. Let’s take a look at a very simple example understand it.
Imagine that we want to implement the data structure of a stack. To begin, we write an interface Stack
, which defines the methods we want to publicly expose for this data structure. These would be:
- The
push
method allows us to add an element on top of the last element that has been added. - The
pop
method is responsible for removing the last element added to the stack, the one at the top. - A
peek
method that returns the last added element, again the one at the top. - And a
size
method that returns a numerical value representing the number of elements in the stack.
interface Stack<ItemType> {
push: (item: ItemType) => void;
pop: () => ItemType | undefined;
peek: () => ItemType | undefined;
size: () => number;
}
As you can see, this interface takes a type as a parameter called ItemType
, allowing us to reuse it regardless of the data type we want to stack.
The way to use this generic interface would be to specify the type of element we are going to use.
#4 – Take Advantage of Type Inference in TypeScript
While it’s essential for 100% of our code to have an assigned type to ensure it works correctly, we don’t have to add annotations to every variable or function return value explicitly.
Type inference in TypeScript is a feature that calculates the corresponding type for a variable as long as it has enough information to deduce it from use.
The easiest way to illustrate it is through an Array. In the following assignment to the variable x
, TypeScript is able to infer that the type that corresponds to that variable is an Array that accepts elements of both type number
and type null
. In this case it would be redundant to also add the type annotation to this variable.
const x = [1, null];
Another common scenario occurs with function return types. In the following function, since both parameters are of numeric types and the value returned by this function is the sum of the two parameters, there is enough information to ensure that the return type of this function will always be a number. So, we wouldn’t need to add that annotation either.
function sum(a: number, b: number) {
return a + b;
}
With each new version of TypeScript, type inference becomes more powerful and covers more complex scenarios. This allows developers to write less code without sacrificing the assurance that we will receive the expected types.
At this point, some people prefer to explicitly specify all types, and that’s something you can configure consistently using TypeScript ESLint.
#5 – Use Libraries Typed with TypeScript
Although most JavaScript libraries nowadays include their type declarations in TypeScript to make them safer, it is our responsibility as developers to ensure that all the code in our application is strongly typed.
Since we have added TypeScript to our project precisely to reduce errors originating from the dynamic typing of JavaScript variables.
When choosing a library for our project, we should ensure that this meets one of the following conditions:
- If it includes the type definition in its official package.
- If, on the other hand, an updated package defining the library’s API circulates on the internet in a repository like DefinitelyTyped, which is specifically responsible for adding a layer of types to JavaScript packages that officially do not include it.
- Or, in the worst case scenario, if we will have to define the types of the library in a TypeScript declaration file by consulting the source code of the library.
declare module 'prettysize' {
export default function (bytes: number | string, removeSpace?: boolean): string;
}
It’s important not to bypass third-party code because in the end it will be our application that ends up failing.
#6 – Avoid Type Casting
If your intention is to use TypeScript incorrectly and leave the door open to errors and unexpected behaviors in your code, Type Casting is the fastest way to achieve it.
Type Casting is a TypeScript feature that allows us to forcefully assign a type to a variable when we are unsure of the value’s type. When programming in TypeScript, it is relatively easy to end up in a situation where we receive a value of type any
or unknown
because at some point in the trace that the value has traveled, the type has not been specified.
This commonly occurs when we forget to pass a type as a parameter to a Generic that has any
or unknown
as default values or when using a library that has not been strictly typed.
const value: unknown = 1;
const a: string = value as string;
const value: unknown = 1;
const a: string = <string>value;
We can cast variables using the as
keyword followed by the type we want to convert to, or by specifying the type between the “less than” and “greater than” operators. However, the correct approach to handling these cases is to identify the point where typing was lost by following the trace and resolving it. If the issue arises from third-party code, we should build a wrapper that adds the necessary annotations to ensure safe typing without making assumptions and without assuming the associated risks.
As a programmer, I understand that the pressure to finish a project on time and other conditions may tempt you to cast variables to avoid TypeScript compiler compatibility errors. However, I believe these moments reveal the difference between a sloppy developer and those who strive to do their job well.
#7 – Use Type Aliases in TypeScript
Are you taking advantage of all the benefits of Type Aliases? In TypeScript, there are two ways to define the shape of an object:
- By defining interfaces that we can extend using the
extends
keyword in a way very similar to classes in JavaScript. - And through Type Aliases, using the
type
keyword followed by an identifier to which we assign the type we are defining. The way to extend these Type Aliases is by using operators to combine them, forming intersections or unions.
interface Indexable {
id: number;
}
interface Book extends Indexable {
title: string;
}
interface Indexable {
id: number;
}
type Book = Indexable & {
title: string;
}
It is true that in simpler cases, using an alias or an interface to define types makes little difference. However, when we need to create complex types by combining them with other defined types, the use of Type Aliases becomes essential to ensure primarily the following two characteristics in our code:
- The project’s readability is enhanced by giving meaningful names to complex types, making the code easier to understand and keeping other definitions in which this type is involved clean.
- We avoid duplicating type definition logic. The composition of these types derived from others is reused by exporting the Type Aliases and using them wherever needed.
interface File {
id: number;
name: string;
}
interface Folder {
id: number;
name: string;
color: string;
}
export type DriveItem = File | Folder;
If you are having difficulties defining types and you overuse interfaces because you did not know the Type Aliases in your code, take a look at the official documentation, because this is probably one of the pieces you are missing to make development in TypeScript more pleasant and to scale your project in a sustainable way.
Conclusion
These are just 7 of the many good practices we could list to improve your TypeScript code.
Adding types to our JavaScript code is a quality improvement that helps filter out a significant portion of runtime errors. However, we must do it correctly to avoid creating false positives, as in the case of Type Castings, which, while passing TypeScript validation, introduce unexpected behaviors and errors into our code.
Furthermore, we have also seen that while setting up TypeScript in your project is easy, some type definitions can become exceedingly complex. Practice or consulting other people’s code are the best teachers to master all the features of TypeScript and acquire the mindset required for type definition.
If you found this content helpful, you can support me by subscribing to my YouTube channel and following me on social media.