TypeScript’s influence is growing by the day. It is now the go-to companion tool for any new web/Node project. The benefits of using TypeScript cannot be overstated. However, it is important to know and understand all the tools that this superset of JavaScript has at our disposal.
Are you investing time in improving your Typescript skills? Are you trying to make the most out of it? Sometimes by not using the right TypeScript’s features and not following its best practices, there can be a lot of code duplication and boilerplates.
Here we look at the five most important features that TypeScript can empower us with. By making sure we understand them and know their use cases, we can build a better and more comprehensive codebase.
1. Unions
Unions are one of the most basic and easy-to-use TypeScript features. They let us easily combine multiple types into one. Intersection and union types are some of the ways in which we can compose types.
function logIdentifier(id: string | number) {
console.log('id', id);
}
They are very useful when we want to express that a certain type is nullable:
|
Unions are not limited to just undefined or primitives. They can be used for any interface or type.
|
Given a union type like above, how can we distinguish between Bike and Plane? By using the discriminated union feature. We will create an enum named Vehicles and use it as a property value.
Let’s see how our code looks with it:
|
We have just seen how unions are a simple yet powerful tool that has a few tricks. However, if we want to express types/interfaces in a more powerful and dynamic way, we need to use generics.
2. Generics
What is the best way to make our methods/APIs reusable? Generics! This is a feature found in most typed languages. It lets us express types in a more general way. This will empower our classes and types.
Let’s start with a basic example. Let’s create a method to add any defined types to an array:
|
What if we want to create the same utility for an int type? Should we redo the same method? By simply using generics, we can reuse the code instead of adding more boilerplate:
|
How can we prevent unwanted types from being using in T? We can use the extends keyword for that purpose:
|
Generics will empower us to build comprehensive and dynamic interfaces for our types. They are a must-master feature that needs to be present in our daily development.
3. Tuples
What are tuples? Let’s look at the definition:
“Tuple types allow you to express an array with a fixed number of elements whose types are known, but need not be the same. For example, you may want to represent a value as a pair of a string and a number.” — TypeScript’s docs
The most important takeaway is that those arrays are of fixed value length. There are two ways to define a tuple:
Explicitly:
const array: [string, number] = ['test', 12];
Implicitly:
const array = ['test', 12] as const;
The only difference is the as const will make the array read-only, which is usually preferable.
Note that tuples can be labelled as well:
|
Labels don’t require us to name our variables differently when destructuring. They’re purely there for documentation and tooling. Labels will help make our code more readable and maintainable.
Note that there’s one important rule when using labelled tuples: When labelling a tuple element, all other elements in the tuple must also be labelled.
4. Mapped Types
What are mapped types anyway? They are a way to avoid defining interfaces over and over. You can base a type on another type or interface and save yourself the manual work.
“When you don’t want to repeat yourself, sometimes a type needs to be based on another type. Mapped types build on the syntax for index signatures, which are used to declare the types of properties which has not been declared ahead of time.” — TypeScript’s docs
To summarise, mapped types allow us to create new types based on existing ones.
TypeScript does ship with a lot of utility types, so we don’t have to rewrite those in each project. Let’s look at some of the most common: Omit, Partial, Readonly, Exclude, Extract, NonNullable, and ReturnType.
Let’s see one of them in action. Let’s say we want to convert all the properties of an entity named Teacher to readonly. What utility can we use? We can use the Readonly utility type:
|
Let’s recap how Readonly works under the hood:
type Readonly
Let’s now create our custom utility for fun. Let’s invert the Readonly type to create a Writable one:
|
Note: Notice the - modifier. In this scenario, it was used to remove the readonly modifier. It can be used to remove other modifiers from attributes like ?.
5. Type Guards
Type guards are a set of tools that help us narrow down the type of objects. This means that we can go from a more general type to a more specific one.
There are multiple techniques to perform type guards. For this article, we will be just focusing on user-defined type guards. Those are basically assertions — like functions for any given type.
How can we use them? We just need to define a function whose return type is a type predicate and it returns true/false. Let’s see how we can turn a typeof operator into a type guard function:
|
Note that if the isNumber check is false, TypeScript can assume that value will be a string since x could be string or number.
Let’s look at another example of a type guard using a custom interface:
|
Note that the isHunter function’s return type is x is Hunter. That assertion function will be our type guard.
Type guards are scoped. Inside the isHunter(x) code block, the x variable is of type Hunter. That means we can safely call its hunt method. However, outside this code block, the x type will be still unknown.
Final Thoughts
We just explored the most important Typescript features we have at our disposal. This is meant to be just an overview that has just scratched their surface to get you curious and showcase what Typescript is capable of. By trying to progressively adopt them, you will see how your code becomes tidier, cleaner, and easier to maintain.