Five useful syntaxes in TypeScript

TypeScript allows developers to write code that gets compiled to JavaScript. The output JavaScript code runs in browsers and anywhere where JavaScript runs. TypeScript is around since 2012. So, it is not something new. Most developers with JavaScript background use TypeScript these days as it is very convenient as well as very safe. In this article, I will show 5 useful syntaxes in TypeScript. But before that, I will show something that we will never use!

Never type

There is a type called never in TypeScript. It is used to mark functions that never return.

function throwError(message: string): never {
  throw new Error(message);
}

Why did I start this article with a type that we won’t use at all? I want to highlight that there are features in TypeScript that have marginal utility. Maybe, interviewers ask these trivia to screen candidates while hiring.

This article is not about trivia. It is about useful syntaxes that make a difference. So, let’s not waste time and get to some nice code.

A) Enum type

Enum, short for Enumerations, is a type that allows to store constant values. When we are working with Redux, we expect each action to have a type key. This type key is a string. Because of string constants floating all over the code, there is a good chance of typos. And so, our code won’t work sometimes. Enum type solves all these problems. To declare an enum, we use code like so:

enum ActionType {
  GetDataPending,
  GetDataSuccess,
  GetDataError
}

Note that there is no “=” sign between ActionType and the opening curly braces. To use this action type, we can export it like we do normally.

export default ActionType;

While creating an action, our code like so:

const action = {
  type: ActionType.GetDataPending
};

The default implementation of enum makes the constants within it as numbers. So, in our case, GetDataPending is 0, GetDataSuccess is 1 and GetDataError is 2. If we want to make them as strings, it is possible. Just assign those string constants as follows:

enum ActionType {
  GetDataPending = 'GET_DATA_PENDING',
  GetDataSuccess = 'GET_DATA_SUCCESS',
  GetDataError = 'GET_DATA_ERROR'
}

It is also possible to mix strings and numbers. But that will make the code less maintainable.

B) Tuple vs Array

We declare an array like so:

const stringArray: string[] = [];
const numberArray: number[] = [];
const hetroArray: (number | string)[] = [];

In the above example, we have an example of an array of strings, an array of numbers or an array of strings or numbers. In the third array, we can have values like ['hi', 100]. If we want to store null, we can define a type string | null and use it as the array type. Array types are flexible.

In TypeScript, we also have tuple type. These are fixed length arrays. We define a tuple like so:

const person: [string, number] = ['Vijay', 44];

In the above example, person is a tuple. This is another example where type inference won’t work. If we don’t define a tuple type for the above initialisation, then person will be an array of strings or numbers. Our type annotation helps TypeScript compiler a lot. Because of the annotation, we can’t add a third value to person.

To access the value within the tuple, use the index: person[0]. Another way to access the first element is using destructuring.

const [name, age] = person;

React uses a tuple as the return type in the useState hook.

C) Function annotation

TypeScript compiler can’t figure out the types of function parameters. It can however understand the return statements within a function and infer the return type. However, I will feel that it is a good practice to annotate both the function parameters and the return type.

function add(n1: number, n2: number): number {
  return n1 + n2;
}

To pass functions around, we use the function type:

function doOperation(operation: (n1: number, n2: number) => number, n1: number, n2: number): number {
  return operation(n1, n2);
}
...
doOperation(add, 10, 20);

In the above code, we pass the add function as a parameter to doOperation function. This function performs an operation on the two numbers. If we pass in the add function as a parameter, the doOperation function simply adds two numbers. Don’t worry about this code. This is just to illustrate how passing functions around is a pain.

Interface solves the problem of passing functions around. Wrap the function in an interface and pass the interface around.

interface Operation {
  (n1: number, n2: number): number;
}

function doOperation(operation: Operation, n1: number, n2: number): number {
  return operation(n1, n2);
}

doOperation(add, 20, 30);

Interfaces that define only the function signature are treated special. They are like function types. In the above example, the add function is equivalent to Operation interface or Operation function type. Defining a function signature in interfaces is very different from an interface that has a function in it. For example, consider the following Car interface.

interface Car {
  drive(speed: number): void;
}

const car: Car = {
  drive(speed: number) {
    console.log('I am driving the car at ' + speed);
  }
};

car.drive();

There is an interface and an object implementing the interface. The object should have a drive function within it. This is very different from the earlier interface that we saw. The Operation interface has only a function signature and no function name. So, the Operation interface is a function type. Whereas the Car interface enforces objects to define the drive function.

D) Constructor shorthand

TypeScript has classes. What is the big deal? Even JavaScript has classes!

In addition to what we know about classes in general, we have access modifiers in TypeScript. So, we can mark some properties as internal to the class and not available outside the class.

class Car {
  private speed: number = 0;
}

const car = new Car();
car.speed = 50; // this is not possible!

In the above example, the property speed is private to the class. So, it is not available outside the Car class. So, if we instantiate a car variable and try to access the speed property, it is not possible to access it. It throws a compilation error. There is also a protected access modifier. The usage has a similar meaning as any other object oriented programming language like C++ or C#.

I am not a big fan of private, protected or public access modifier. But I like constructor shorthands. I will explain that briefly.

If you look closely at the Car class and the speed property, we initialise the speed property to 0. If we don’t initialise the speed property, the compiler will flag it as an error. There is a work-around to it. We can leave the speed property uninitialised if we define a constructor like so:

class Car {
  private speed: number;

  constructor(speed: number) {
    this.speed = speed;
  }
}

const car = new Car(50);

Defining a constructor with a parameter enforces the developer to specify the value of the property at the time of object creation. But that is not all. See this code below.

class Car {
  constructor(private speed: number) {}
  drive() {
    console.log("I am driving at " + this.speed);
  }
}

const car = new Car(50);
car.drive();

The constructor has a private access modifier to the parameter. And its body is empty. So, what does it do? That is what we call constructor shorthand. The constructor automatically defines a private property – speed and assigns the speed parameter to the private property. That is really short and sweet code.

E) Constraints on Generic Types

Generic type allows developers using that type to specify the type related to that generic type. For example, Array is a built-in generic type. An array can hold strings or numbers or other types. But the developer specifies the concrete type that the array should hold.

const numbersArray: number[] = []; // or
const numbersArrayEquivalent: Array<number> = [];

In the above code, Array is a generic type and the developer specifies the type “number” that the Array can hold. This tells the compiler that numbersArray can hold an array of numbers and only numbers. So, when we access an element from the array, auto-complete is available.

Consider the code below:

function merge<T, U>(obj1: T extends object, obj2: U extends object): T | U {
  return Object.assign(obj1, obj2);
}

The above code is intimidating. Just like we have generic type, we have generic function. Generic functions can customise the type of parameters that we pass to it. In this case, the merge function accept two parameters, both of which are generic. The parameter obj1 is of type T. And the parameter obj2 is of type U. Developer using this function will specify the type for these parameters T and U when they call this function.

const person = merge({ firstName: 'Vijay' }, { lastName: 'T' }); 

With the above code, the developer calls the merge function. For parameter obj1, he provides an object having the firstName key. And for parameter obj2, he provides an object having the lastName key. The merge function combines the two objects into a single object having the keys: firstName and lastName. TypeScript has an intersection type. T | U represents an object that has keys from both T and U. In this case, the keys: firstName and lastName.

There is one final detail about the parameters: obj1 and obj2. They have a constraint (T extends object). This constraint suggests that the type T and the type U must be an object. That is because if we pass primitive types like string, number or boolean, the Object.assign function silently fails. Object.assign function needs both the parameters to be of type object to function correctly.


Summary

TypeScript is amazing. It has nice cool syntax that any developer will appreciate. In this article, we saw a few of them: Enums, Tuples, Function annotations, Constructor shorthands and Constraints on Generic types. Hope it was enjoyable to read. For more documentation, visit the official TypeScript site.

Related Posts

One thought on “Five useful syntaxes in TypeScript

  1. In your article, you mentioned that TypeScript offers a ‘never’ type which is used to mark functions that never return. This can be helpful in scenarios where a function always throws an error or enters an infinite loop. Could you elaborate on real-world use cases where utilizing the ‘never’ type can lead to more robust code, and how it differs from using other return types like ‘void’ or throwing exceptions?

Leave a Reply

Your email address will not be published.