DEV Community

Hari Krishnan
Hari Krishnan

Posted on

Typescript - Advanced Types

1. Intersection types

Intersection types allow us to combine multiple types into one type. In the below code consider we combine the left and the right type to get the combined Intersection Type.

type LeftType = {
  id: number;
  left: string;
};

type RightType = {
  id: number;
  right: string;
};

type IntersectionType = LeftType & RightType;

const joinedValue: IntersectionType = {
  id: 1,
  left: "left value",
  right: "right value",
};
Enter fullscreen mode Exit fullscreen mode

This intersection can also be achieved using interfaces. With the above code, left and right can be separate interfaces and we can have an intersection interface which implements both the interfaces.

2. Type Guards (typeof, in , instanceof)

typeof

Consider we have a union type which holds two other types. And also consider you need to add or concatenate based on whether it is a number or string, how to identify in runtime whether the incoming value is a string or a number ?

type UnionType = string | number;

function add(a: UnionType, b: UnionType) {
  return a + b; 
  // error: Operator '+' cannot be applied to types 'UnionType' and 'UnionType'.
}
Enter fullscreen mode Exit fullscreen mode

Typescript cannot add or concatenate because it cannot infer the exact type it received in the add method. Type guards can be used here to check the type in runtime and add or concatenate the values accordingly. So the method will be modified as

function add(a: UnionType, b: UnionType) {
  if (typeof a === "string" || typeof b === "string") {
    return a.toString() + b.toString();
  }
  return a + b;
}
Enter fullscreen mode Exit fullscreen mode

in

This keyword is provided by javascript itself. The in keyword can be used in cases where we have to check if an object has a property we are looking for in it.

Consider the same example above where we had two types the left and the right. Now we want to have a common print method which will print the information based on the left or right type we pass as input to the method.

Image description

We can find from the above image that typescript cannot identify the left and right properties as they can vary based on the input given to the method. In this case we can make use of the in keyword to check if the properties are present, and then typescript will be satisfied to ensure that the properties are present and it will print them.

type LeftType = {
  id: number;
  left: string;
};

type RightType = {
  id: number;
  right: string;
};

function printTypeInformation(typeInfo: LeftType | RightType) {
  console.log("id : " + typeInfo.id);
  if ("left" in typeInfo) {
    console.log(typeInfo.left);
  }
  if ("right" in typeInfo) {
    console.log(typeInfo.right);
  }
}
Enter fullscreen mode Exit fullscreen mode

instance of

Consider we have two classes one as the Car and the other as the Truck. Both the classes will have a common method to drive and only the truck will have the loadGoods method.

We also have a common type called Vehicle which contains the union of Car and Truck.

Consider the case we want to have a method called useVehicle and we want to make use of all the methods available in the vehicle instance.

Now here also there is a situation where we cannot make use of the loadGoods method as it cannot be inferred by typescript. In this case we can make use of the instanceof to check in runtime and access their methods accordingly.

class Car {
  drive() {
    console.log("Driving a car");
  }
}

class Truck {
  drive() {
    console.log("Driving a truck");
  }

  loadGoods(amount: number) {
    console.log("Loading cargo : " + amount);
  }
}

type Vehicle = Car | Truck;

const car = new Car();
const truck = new Truck();

function useVehicle(vehicle: Vehicle) {
  vehicle.drive();
  if (vehicle instanceof Truck) {
    vehicle.loadGoods(100);
  }
}

useVehicle(car);
useVehicle(truck);
Enter fullscreen mode Exit fullscreen mode

An important point to note here instead of classes we cannot apply the same for interfaces, because only classes are compiled to something that javascript can understand but javascript does not know what an interface is.

3. Discriminated Unions

A special type of type guard. It is a pattern which we can use when working with union types that makes implementing type guards easier.

There are disadvantages in using the in keyword as well as the instance of keyword. When using the in keyword we might do a typo in specifying the property name and in case of the instanceof keyword, we cannot apply the same for interfaces.

So discriminated union comes handy with addressing all the disadvantages. It is a suggested pattern to follow when we want identify types in runtime.

We simply add a type which would denote the type in the form of a string. And we use switch statement over this type to identify the type in runtime.

interface Car {
  type: "Car";
  carSpeed: number;
}

interface Truck {
  type: "Truck";
  truckSpeed: number;
}

type Vehicle = Car | Truck;

function printVehicleSpeed(vehicle: Vehicle) {
  let speed;
  switch (vehicle.type) {
    case "Car":
      speed = vehicle.carSpeed;
      break;
    case "Truck":
      speed = vehicle.truckSpeed;
  }
  console.log("vehicle speed : " + speed);
}

Enter fullscreen mode Exit fullscreen mode

How can discriminated unions help in avoiding typos ?

Image description

When any union type property is used in a switch statement, as in the above image, typescript intelligently identifies the type and shows suggestions.

4. Utility Types

Typescript provides utilities that help to manipulate types easily. In order to use them, you need to pass into the <> the type you want to transform.

Partial<T>

It helps in making all the properties of a particular type optional. It will add a ? mark next to every field.

// initially all props in the below type are required by default
type User = {
  id: number;
  firstName: string;
  lastName: string;
};

const printUser = (user: Partial<User>) => {
  console.log(user);
};

printUser({ id: 1 });

printUser({ firstName: "first name", lastName: "last name" });
Enter fullscreen mode Exit fullscreen mode

Since the printUser method received only a partial user type, we need not specify all the props.

Required<T>

It is opposite to the Partial utility, it makes all props of a particular types mandatory.

// lastName property is optional in the below User type
type User = {
  id: number;
  firstName: string;
  lastName?: string;
};

const printUser = (user: Required<User>) => {
  console.log(user);
};

printUser({ id: 1, firstName: "first name", lastName: "last name" });
Enter fullscreen mode Exit fullscreen mode

Since the printUser method expects the User type with Required utility, you have to always provide all the props in the User Type.

Readonly<T>

The readonly utility will help in transforming all properties of type T, in order to make them not reassignable with a new value.

type User = {
  id: number;
  firstName: string;
  lastName: string;
};

const printUser = (user: Readonly<User>) => {
  console.log(user);
  // user.id = 2;
  // cannot assign value to any prop in user, as it is readonly
};

printUser({ id: 1, firstName: "first name", lastName: "last name" });
Enter fullscreen mode Exit fullscreen mode

Here the print user method receives only a readonly type and inside the method, no property of the user type can be reassigned with a value.

Top comments (0)