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:

function logIdentifier(id: string | undefined) {

  if(!id) {

    console.error('no identifier found');

  } else {

    console.log('id', id);

  }

}


Unions are not limited to just undefined or primitives. They can be used for any interface or type.

interface Vehicle {

  speed: number;

}

interface Bike extends Vehicle {

  ride: () => void;

}

interface Plane extends Vehicle {

  fly: () => void;

}

function useVehicle(vehicle: Bike | Plane) {

  ...

}


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:

enum Vehicles {

    bike,

    plane

}

 

interface Vehicle {

    speed: number;

    type: Vehicles;

}

 

interface Bike extends Vehicle {

    ride: () => void;

    type: Vehicles.bike;

}

 

interface Plane extends Vehicle {

    fly: () => void;

    type: Vehicles.plane;

}

 

function useVehicle(vehicle: Bike | Plane) {

    if (vehicle.type === Vehicles.bike) {

        vehicle.ride();

    }

 

    if (vehicle.type === Vehicles.plane) {

        vehicle.fly();

    }

}


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:

function addItem(item: string, array: string[]) {

  array = [...array, item];

  return 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:

function addItem(item: T, array: T[]) {

  array = [...array, item];

  return array;

}

 

addItem('hello', []);

 

addItem(true, [true, true]);


How can we prevent unwanted types from being using in T? We can use the extends keyword for that purpose:

function addItem(item: T, array: T[]) {

  array = [...array, item];

  return array;

}

 

addItem('hello', []);

 

addItem(true, [true, true]);

 

addItem(new Date(), []);

//      ^^^^^^^^^^

// Argument of type 'Date' is not assignable to parameter of type 'string | boolean'


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:

function foo(x: [startIndex: number, endIndex: number]) {

  ...

}


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: OmitPartialReadonlyExcludeExtractNonNullable, 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:

interface Teacher {

  name: string;

  email: string;

}

 

type ReadonlyTeacher = Readonly;

 

const t: ReadonlyTeacher = { name: 'jose', email: 'jose@test.com'};

 

t.name = 'max'; // Error: Cannot assign to 'name' because it is a read-only property.(2540)


Let’s recap how Readonly works under the hood:

type Readonly = { readonly [P in keyof T]: T[P]; }

Let’s now create our custom utility for fun. Let’s invert the Readonly type to create a Writable one:

interface Teacher {

  readonly name: string;

  readonly email: string;

}

 

type Writeable = { -readonly [P in keyof T]: T[P] };

 

const t: Writeable = { name: 'jose', email: 'jose@test.com' };

 

t.name = 'max'; // works fine


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:

function isNumber(x: any): x is number {

  return typeof x === "number";

}

 

function add1(value: string | number) {

  if (isNumber(value)) {

     return value +1;

  }

  return +value + 1;

}


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:

interface Hunter {

    hunt: () => void;

}

 

// function type guard

function isHunter(x: unknown): x is Hunter {

    return (x as Hunter).hunt !== undefined;

}

 

const performAction = (x: unknown) => {

  if (isHunter(x)) {

    x.hunt();

  }

}

 

const animal = {

  hunt: () => console.log('hunt')

}

 

performAction(animal);


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 variable is of type Hunter. That means we can safely call its hunt method. However, outside this code block, the 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.

Most-Read Articles

10 Tips To Become A Better Software Engineer

2nd April, 2021

Practical advice from a programmer with more than 12 years in the field.

Read more

How To Become A Golang Developer: A 6-Step Career Guide

6th April, 2021

Go is an in-demand language across the board. Here are the steps to become an employable and modern Go developer.

Read more

5 Most Asked Qs In The Amazon Software Engineer Interview

11th June, 2021

Analyzing over 1,300 personal experiences of those who’ve gone through the process.

Read more

World-class articles, delivered weekly.