Everyday types in TypeScript

Everyday types in TypeScript

As we explored in the last article, we have learned about TypeScript, why we use it, how it differs from JavaScript, how to install, configure, and run TypeScript, and some basic types in TypeScript.

In this article, we’ll learn in-depth about some of the most common types in TypeScript. They include object, Array, Union types, enums, tuples, any type, Literal types, and Type Aliases.

typescript-1.png

object TYPE:

DEFINITION:

An object is a type of all non-primitive values (primitive values are undefined, null, booleans, numbers, and strings).

To define an object type, we simply list its properties and their types. To understand this, take a look at the following examples mentioned below:

IMPLICIT TYPE:

Firstly, open VS Code and create an index.ts file with the following code:

const course = {
  name: "Learn typescript",
  price: 20,
};

console.log(course);

As you hover over the course variable, you will see TypeScript has inferred the type of that variable.

Even though we haven't explicitly stated that the course variable will have name and price keys, the TypeScript type inference system handles writing these types.

image.png

Visually, it looks like a JavaScript object. However, if you notice it has ; at the end of the key-value pair, and the value of the key is nothing but a type of that particular key. The name key is of type string and the price key is of type number.

You will see autosuggestions from your IDE if you add a . after the course variable since the IDE is aware of the keys the course variable possesses.

image.png

The Compilation error with a descriptive message will appear if you attempt to access the key that does not exist in the course object. This is unlike JavaScript, which ignores the error without warning.

image.png

EXPLICIT TYPE:

Instead of having TypeScript infer the type for us, let's explicitly specify what type the course variable must have.

const course: {
  name: string;
  price: number;
  isPublished: boolean;
} = {
  name: "Learn typescript",
  price: 20,
};

console.log(course.name);

image.png

We have assigned a boolean type with the key name isPublished to the course variable. Since we explicitly stated that the course variable must have name, price, and isPublished properties, TypeScript detects this as a compilation error since we are only passing the name and price.

Let's say you want to keep the isPublished property optional. In that case, you can add a ? before : to make that property optional as follows:

image.png

Furthermore, if you hover over the isPublished key, you will see that TypeScript has inferred its type as boolean | undefined. The reason is that isPublished is optional, and you may not pass any value to it. When that happens, you will receive an undefined value, and for that TypeScript warns you that the value of isPublished maybe undefined.

isPublished is called an optional property in the above example. In contrast, boolean| undefined is a union type which we will be exploring at great length in this blog.

NESTED object types:

JavaScript also supports nested objects, which means objects can be nested within objects. Let's take a look at how we assign a type when dealing with such nested objects:

const course: {
  name: string;
  price: number;
  isPublished?: boolean;
  student: {
    name: string;
    age: number;
  };
} = {
  name: "Learn typescript",
  price: 20,
  student: {
    name: "John",
    age: 30,
  },
};

console.log(course);

In the above example, we have course which is an object, inside which we have the student key, which itself is another object with the name and age keys.

So now, if you try to access student, you will also get autosuggestions for the student object which is nested inside the course object.

image.png

Also, typescript knows the type of values that we are getting in all the keys of the course variable so it can also suggest possible methods for a particular type of values:

image.png

Array TYPE:

DEFINITION:

Array refers to a special type of data type that can store multiple values of different types sequentially using a special syntax.

SHORTHAND SYNTAX type[]:

Take a look at the following code:

const course = {
  name: "Learn typescript",
  price: 20,
  tags: ["typescript", "javascript"],
};

console.log(course);

Now, when you hover over tags, you will see that it says string[]

image.png

It is a shorthand and widely used syntax when assigning a type to an array. A string[] notation implies that the array contains only string elements.

Since "typescript" and "javascript" are strings, it infers tags key as a string[] (array of strings).

GENERIC SYNTAX Array<type>:

Another way to specify a type for an array is to use generic syntax, such as:

const course: {
  name: string;
  price: number;
  tags: Array<string>; // this is generic syntax Array<type_of_elements>
} = {
  name: "Learn typescript",
  price: 20,
  tags: ["typescript", "javascript"],
};

console.log(course);

Here, we specify that tags is an Array that contains only string type elements.

The <> indicates that this is a Generic type, and we must specify the type of elements to include in this array. (We will study Generic types in depth in future blogs)

The following error will occur if the element type is not provided:

image.png

TypeScript generally infers the type of an array with shorthand syntax

Array WITH MIXED TYPES:

What if the array has elements of different types? For instance,

const course = {
  name: "Learn typescript",
  price: 20,
  tags: ["typescript", "javascript", 20, 10],
};

Hovering over tags reveals its type as (string | number)[] (Again, string | number is a union type, which means the array includes elements which can be of type string or number).

image.png

We need something called type guards for handling union types. This is a bit advanced, so we will cover it in a later blog post.

Array OF object:

The types of objects that should be included in an array can be explicitly specified. This allows us to avoid runtime errors and speed up development.

To understand how to define a type for an array of specific objects, let's take an example:

const course1 = {
  name: "Learn typescript",
  price: 20,
  tags: ["typescript", 20, 10],
};

const course2 = {
  name: "Learn javascript",
  price: 20,
  tags: ["javascript", 10],
};

const courses: {
  name: string;
  price: number;
  tags: (string | number)[];
}[] = [course1, course2];

console.log(courses);

We have two objects here, course1 and course2, whose type is { name: string; price: number; tags: (string | number)[]; }.

Then we have another variable courses that contains a list of these two courses, so let's create an array with each element being of a type { name: string; price: number; tags: (string | number)[]; }

The type is now known, and we know that courses should be Array of type { name: string; price: number; tags: (string | number)[]; }

Therefore, we assigned { name: string; price: number; tags: (string | number)[]; }[] as the type of courses variable.

As shown below, TypeScript will automatically infer the type for you if you don't explicitly mention it:

image.png

Because TypeScript knows what type of element the courses array has, you will get autosuggestions not only for courses but also for each element in the courses array when you perform any operation on it.

image.png

tuple TYPE:

DEFINITION:

A tuple type is a type of Array that knows how many elements it contains, as well as the type of elements contained at specific positions.

Here is how we can declare a variable as a tuple:

let course: [number, string] = [1, "Typescript"];

Here, we are saying that the course is an array of fixed length and must have the first element of type number and the second element of the type string.

If you try to replace the element at the index 0 which is of type number with a string, you will get a compilation error because we explicitly specified that the element at the index 0 must be a number.

image.png

EXCEPTION IN tuple:

A tuple should indeed be immutable, but it is still possible to push an element in it because TypeScript doesn't throw a compilation error when you call the .push() method.

image.png

However, if you attempt to reassign the variable with a different structure, you will receive the following compilation error:

image.png

When reassigning, you need to follow the same structure as when assigning.

POSSIBLE USE CASE OF tuple:

If you need exactly x amount of values in an array, and you know the type of each element at a specific index, then you may want to consider using tuple type instead of an Array type, for more strictness.

enum TYPE:

DEFINITION:

Enums are one of the few features TypeScript has which is not a type-level extension of JavaScript.

By using enums, we can create sets of constants with names. You can create cases and documentation more easily by using enums.

NUMERIC ENUMS:

An enum can be defined using the enum keyword. Take a look at the following code:

enum Role {
  ADMIN = 1,
  READ_ONLY,
  AUTHOR,
}

const user: {
  name: string;
  age: number;
  role: Role;
} = {
  name: "Max",
  age: 30,
  role: Role.ADMIN,
};

We have a numeric enum where ADMIN is initialized with 1. From that point forward, all of the following members will be auto-incremented. Thus, Role.ADMIN has the value 1, READ_ONLY has 2, and AUTHOR has 3.

Once we define an enum type, in the above case its Role, we don't need to remember what the value of any particular role is, we can simply access enum variables with their names such as Role.ADMIN or Role.READ_ONLY or Role.AUTHOR.

You will also get autosuggestions from your IDE to do so as follows:

image.png

In addition, if you want to change values for the enum variable, you can do it in your enum type Role. You don't have to make that change everywhere in the code.

STRING ENUMS:

In the same way as numeric enums, enums can have strings as values. Here's an example:

enum Role {
  ADMIN = "admin",
  READ_ONLY = "read_only",
  AUTHOR = "author",
}

As the logic remains the same, but in numeric enums you will get 1 in return from Role.ADMIN, as in string enums you will get "admin".

It is up to you what you want to assign to each enum.

HETEROGENEOUS ENUMS:

Similarly, you can assign mixed types of values to enums as follows:

enum Role {
  ADMIN = 1
  READ_ONLY = "read_only",
  AUTHOR = 3,
}

You get the point!

any TYPE:

DEFINITION:

When a value is of type any, you can access any property of it, call it like a function, assign it to a value of any type, or pretty much anything else as long as it's syntactically legal.

WHY NOT USE any:

While any type is flexible, it loses all the advantages that TypeScript offers. As a result, it provides the same experience as vanilla JavaScript, thus eliminating the need for TypeScript.

Take a look at the following code:

let person: any = {
  name: "John",
  age: 30,
};

console.log(person.canWalk())

In the above example, we are calling the canWalk() function, which does not exist in the person variable. However, TypeScript is simply ignoring everything about that particular variable, so we aren't getting any compilation errors.

There is no autocompletion for this variable, which is another downside.

image.png

This is a disadvantage of using any type and you should avoid using any unless you don't know the type of the variable or parameter.

USE CASE OF any:

  • When there are no other options, and no type definitions are available for the piece of code you're working on, choose the any type.

  • any has some practical use cases. For example, if you are getting a response from an API call and you don't know the structure of the response, then you can use any to disable the type-checking for the response object.

  • However, if you know the type, be explicit about it.

UNION TYPES:

DEFINITION:

TypeScript’s type system allows you to build new types out of existing ones using a large variety of operators. We can create new custom types by combining some of the core types.

SYNTAX:

The | operator enables us to combine different types and create a new one. In the previous topics, we used this concept:

let strOrNumArray: (string | number)[] = [];

In the above example, we are saying that the strOrNumArray could contain elements of type either string or number

So, you won't get a compilation error if you insert elements of type 'number' or 'string'. However, adding a boolean type will result in the compilation error as follows:

image.png

The above code can be made valid by adding another type boolean after string and number to make an array that has the possibility of string, number, and boolean, as follows:

image.png

HANDLE COMPILATION ERRORS USING TYPE GUARDS:

When you use union types, you will get a compilation error if the operation is only valid for one of the types you specified.

Let's see an example to understand this. take a look at the following code:

const combineStrOrNum = (param1: string | number, param2: string | number) => {
  let result = param1 + param2; // compilation error here
  return result;
};

In the above example, by stating that the param1 and param2 are both of type string | number, we mean that there may be cases when you pass param1 as number and param2 as string, and as we saw in our blog on Introduction to TypeScript, using + between types string and number can lead to unexpected behavior.

So, typescript complains that this expression can yield unexpected value.

We can handle this with type guards, which are available in JavaScript as well. Look at the following code:

const combineStrOrNum = (param1: string | number, param2: string | number) => {
  if (typeof param1 === "number" && typeof param2 === "number") {
    console.log(param1 + param2); // return sum of 2 numbers
  } else if (typeof param1 === "string" && typeof param2 === "string") {
    console.log(param1 + " " + param2); // return concatenation of 2 strings
  } else {
    console.log("Mixed types can not be added"); // return error message
  }
};

combineStrOrNum("Hello", "World"); // Hello World
combineStrOrNum(1, 2); // 3
combineStrOrNum("Hello", 2); // "Mixed types can not be added" (unexpected behavior)

Here, the typeof keyword indicates the type of a particular variable value.

When the type of param1 and param2 is string, then only we want to concatenate them with a space.

And if param1 and param2 are both numerical, we want to add both of them.

Shortly, we are adding some kind of type guard to prevent unexpected behavior.

In future blogs, we'll explore in-depth concepts about type guards and other type guards beside typeof.

LITERAL TYPES:

DEFINITION:

A literal type is a more concrete sub-type of a union type. The difference is that in union types, we specify the types of variables we are expecting, but in Literal types, we specify the exact value we are expecting.

Let's look at the example to understand this:

SYNTAX

let readOnly: "readonly" = "readonly";

Specifically, we are declaring that you can only assign the "readonly" string to the readOnly variable. A string other than "readonly" cannot be assigned to this variable. If you do so, you will get a compilation error as follows:

image.png

USECASE AND IDE SUPPORT:

If you want to accept only predefined values, you can assign a literal type. For example:

let button: {
  label: string;
  severity: "primary" | "secondary" | "danger" | "warning";
  isSubmit: boolean;
} = {
  label: "Submit",
  severity: "primary",
  isSubmit: true,
};

Using the example above, we are creating a button variable that has label - string, isSubmit - boolean, and severity - "primary" | "secondary" | "danger" | "warning".

You will get the following compilation error if you try to assign a different string to the severity key, even if it is of type string.

image.png

A benefit of the Literal type is that you get autosuggestions for that particular key with all the possible values.

Take a look at the following image:

image.png

This is useful in large applications when you don't know which value a given variable can have.

TYPE ALIASES:

WHAT IS IT?:

We might need to use longer types when working with union types or literal types. If we want to add another component apart from the button, let's say an alertBar that is also having the severity of "primary" | "secondary" | "danger" | "warning", it's a bit cumbersome to write it again and again. We can avoid this by using type aliases.

WHY AND HOW TO USE IT?:

Creating dynamic and reusable code is crucial. Don't-Repeat-Yourself (DRY) is an important principle to follow when writing TypeScript code. You can accomplish this using TypeScript aliases.

A custom type can be created by using the type keyword followed by the type's name. Let's look at the example below:

type SeverityType = "primary" | "secondary" | "danger" | "warning";

let button: {
  label: string;
  severity: SeverityType;
  isSubmit: boolean;
} = {
  label: "Submit",
  severity: "primary",
  isSubmit: true,
};

let alertBar: {
  message: string;
  severity: SeverityType;
  duration: number;
} = {
  message: "This is an error message",
  severity: "danger",
  duration: 2000,
};

By creating a type, you can now use SeverityType anywhere in your code as if it were a number, string, boolean, or any of the primitive or reference types. It's a valid type for TypeScript.

TYPE OF AN OBJECT:

type can also represent the structure of an object, such as what fields it should contain.

Creating a custom type for an object is as easy as using the type keyword followed by the type's name and specifying the fields of the type, as well as their types, in the {}.

In the above example, we can create the following custom types for button and alertBar:

type SeverityType = "primary" | "secondary" | "danger" | "warning";

type Button = {
  label: string;
  severity: SeverityType;
  isSubmit: boolean;
};

type AlertBar = {
  message: string;
  severity: SeverityType;
  duration: number;
};

let button: Button = {
  label: "Submit",
  severity: "primary",
  isSubmit: true,
};

let alertBar: AlertBar = {
  message: "This is an error message",
  severity: "danger",
  duration: 2000,
};

We've created two new types in the above code: Button and AlertBar, which are type aliases for button and alertBar, respectively. Now, we can use those types anywhere in our code without having to rewrite them each time.

We will use interfaces instead of types to describe an object's structure in future blogs.

CONCLUSION:

  • In this blog, we've covered some of the most common types of values you’ll find in TypeScript code.

  • Types can also be found in many places other than just type annotations. We reviewed the most basic and common types you might encounter when writing TypeScript code. These are the core building blocks of more complex types, which will be discussed in future blogs.

Make sure to subscribe to our newsletter on https://blog.wajeshubham.in/ and never miss any upcoming articles related to TypeScript and programming just like this one.

I hope this post will help you in your journey. Keep learning!

My Website, connect with me on LinkedIn and GitHub.