Typescript Tutorial Series - 02.Fundamentals

Typescript Tutorial Series - 02.Fundamentals

ยท

12 min read

Hi there ๐Ÿ–๏ธ

My name is Milad and welcome to the second part of the typescript tutorial series. In this part, we are going to explore the fundamentals of typescript. We will learn about built-in types such as any, array, tuple, enum, function and object. The concepts that we are going to learn in this chapter will be the foundation for future parts; So stay with me through this amazing and fun journey.


Built-In Types

As you know, javascript has several built-in data types such as number, string, boolean, null, undefined and object. However, the typescript extends the previous list and introduces other data types such as any, unknown, never, enum and tuple. But before getting started, let's play around with primitive data types in typescript. Open up the index.ts file and remove previous codes. Now let's declare a variable called income with type number:

let income: number = 123456789;

Here, we are annotating the type of variable income using syntax. By the way, in case we have a large number, we can separate its digits using the underscore(_) as follows which makes our code more readable:

let income: number = 123_456_789;

Let's declare two other variables with types string and boolean:

let name: string = "TypeScript";
let is_published: boolean = true;

Since we have initialized our variables, the typescript compiler is smart enough to detect their types so that we don't need to annotate their types directly:

let income = 123_456_789;    // type: number
let name = "TypeScript";    // type: string
let is_published = true;    // type: boolean

Now, if we hover the mouse pointer over variable names, we can realize that their types are not changed. But what if we declare a variable and don't initialize it?

let other;    // type: any

In typescript, we have a type called any which represents any kind of value. If we declare a variable without the initial value, the typescript compiler assumes that its type is any and as a best practice we should avoid that as much as possible.


Let's define a function as follows which logs its parameter in the console:

function logger(doc) {
    console.log(doc);
}

As you can see we have a compilation error on the doc parameter. If we hover the mouse pointer over the function parameter, It says: Parameter 'doc' implicitly has an 'any' type which means we have not set explicitly its type and the typescript compiler tries to guess that. We have two options to turn off this error:

  1. Annotating the function parameter directly as type any as follows:

     function logger(doc: any) {
         console.log(doc);
     }
    

    With this, we tell the typescript compiler that we know what we are doing and the type of the doc parameter is any.

  2. By modifying tsconfig.json file. To do so, open up the configuration file and in the type checking section, uncomment noImplicitAny option and set its value to false. Now if we check the function, the error is gone.


Arrays

Let's talk about arrays. In javascript we can declare an array as follows:

let myArr = [1, 2, "3"];

Since arrays are dynamic in JavaScript, each element can be of a different type. But what if we pass this array to a function that expects a list of numbers? The third element is going to cause an issue and we need to modify our array as follows:

let myArr: number[] = [1, 2, 3];

Since we have initialized our array with all numeric elements, we can remove the type annotation:

let myArr = [1, 2, 3];

Now, what if we declare an empty array as follows:

let myOtherArr = [];

If we hover the mouse pointer over the variable, we can see that its type is any[] which we should avoid. If we want to declare an empty array, we should explicitly apply type annotation:

let myOtherArr: number[] = [];

Another cool benefit of using typescript is code completion or IntelliSense. For example, if we write the code below:

let myArr: number[] = [1, 2, 3, 4, 5];
myArr.forEach(el => el.);

We will get a list of all methods and properties of number object as follows:

Which is very useful for productivity.


Tuples

TypeScript introduces a new type called tuple which is a fixed-length array where each element has a particular type. We often use it in case we have a pair of values. For example:

let person: [number, string] = [1111, "Milad"];

If we add another value to our tuple as follows:

let person: [number, string] = [1111, "Milad", 2];

It will cause a compilation error which says: Type "[number, string, number]" is not assignable to type "[number, string]". Source has 3 elements but target allows only 2.

If we access our tuple first element like this:

person[0].

we will get all methods and properties related to the number object:

And if we access our tuple second element like this:

person[1].

we will get all methods and properties related to the string object:


Tuples are internally represented using plain javascript arrays so that if we compile our index.ts file, we can only see a regular javascript array. Let's compile our code:

$ tsc

Then open up the index.js file, the result will be as follows:

let person = [1111, "Milad"];

So that means, if we write the code below in our typescript file:

person.

We will get all methods and properties that are related to arrays:

By the way, there is an issue. If we go back to our index.ts file and add the following lines to that like this:

person.push(2);
console.log(person);

The code above will add a third element in the array and the typescript compiler is not going to complain about that. Let's re-compile our code and run index.js file using nodejs to see the result:

$ tsc
$ nodejs dist/index.js

# Output:
# [1111, "Milad", 2]

This is one of the gaps in the typescript. As a best practice, we should restrict our tuples to having only two values because having more than two elements will reduce their readability and make them hard to understand.


Enum

We have another built-in type called enum which represents a list of related constants. Let's say we want to represent the directions using constants:

const up = 1;
const right = 2;
const buttom = 3;
const left = 4;

Now let's simplify the code above using the enum type:

enum Direction {Up, Right, Buttom, Left};

The typescript compiler assigns the value of 0 to the first member by default and we need to specify it directly as follows:

enum Direction {Up = 1, Right, Buttom, Left};

We can also set the string values to enum members like this:

enum Direction {Up = "u", Right = "r", Buttom = "b", Left = "l"};

Now let's use our enum:

enum Direction {Up = 1, Right, Buttom, Left};
const myDir: Direction = Direction.Right;
console.log(myDir);

We need to compile the index.ts file and run the generated index.js file using nodejs:

$ tsc
$ node dist/index.js

# Output: 2 --> numeric value associated with enum member

Let's look at the index.js code generated by the typescript compiler:

Don't worry, we don't need to fully understand the code above. By the way, if we declare our enum as a constant like below:

const enum Direction {Up = 1, Right, Buttom, Left};
const myDir: Direction = Direction.Right;
console.log(myDir);

The typescript compiler will generate more optimized javascript code for us. Let's try this:

$ tsc
$ node dist/index.js

# Output: 2

Look at the index.js file once again:


Functions

Let's talk about functions in the typescript and its solutions to prevent common issues with functions. Let's consider we have a function that calculates the tax as follows:

function taxCalculator(income: number) {
    // Without any code
}

If we hover the mouse pointer over the function name, we will see the following hint:

Which means our function does not return any value. If we modify our function like the below:

function taxCalculator(income: number) {
    return 8;
}

and hover the mouse pointer over it once again, this time the typescript compiler infers that the return value is a numeric value which is correct.

But as a best practice, we should annotate directly both the parameters and the return value of functions. To do so, we need to modify our function as below:

function taxCalculator(income: number):number {
    return 8;
}

Or this, if we are going to return nothing :

function taxCalculator(income: number):void {
    // With out any code
}

As you can see in our modified function, the income parameter is unused which can cause errors later on; to detect this kind of issue where we forgot to use function parameters, we need to change the tsconfig.json file as follows.

In Type Checking section, uncomment "noUnusedParams" option and make sure that its value is set to true. Now if we go back to the index.ts file, we can see a warning on the income parameter that says "income is declared but its value is never read."

Let's fix that problem:

function taxCalculator(income: number): number {
    if (income < 50_000) {
        return income * 0.2;    
    }
}

As soon as we type the code above, we can see there is a compilation error on the function return type which says: "function lacks ending return statement and return type does not include undefined."

If the condition is true, the code inside the if block returns a numeric value but otherwise javascript by default returns undefined which is not a numeric value. That's why we get a compilation error on the function return type.

Let's temporarily remove the return type annotation to fix the compilation error:

function taxCalculator(income: number) {
    if (income < 50_000) {
        return income * 0.2;    
    }
}

Now the previous compilation error is gone while our function still has an issue that can cause a bug in the application. To detect this kind of error, once again we need to apply a minor change to the tsconfig.json file. To do so, open up the tsconfig.json file and in the Type Checking section, uncomment the "noImplicitReturns" option and make sure that its value is set to true. Now if we go back to our index.ts file, we can see there is a warning that says: "Not all code paths return a value." This is the final version of our function:

function taxCalculator(income: number):number {
    if (income < 50_000) return income * 0.2;
    return income * 0.3;
}

There is another option in the tsconfig.json file which enables the typescript compiler to detect unused local variables. Let's assume we have a function as follows:

function test(value: string): void {
    let x;
    console.log(value);
}

We have an unused variable inside the test function. To detect this kind of error, open up the tsconfig.json file and in the Type Checking section, uncomment "noUnusedLocals" option and make sure that its value is set to true.


Now let's add the second parameter to our taxCalculator function:

function taxCalculator(income: number, year: number): number {
    if (year < 2020) return income * 0.2;
    return income * 0.3;
}

If we are going to run this function, we need to pass exactly two arguments to it, not more and not less. As you know in javascript we could pass fewer or more arguments to a function and javascript didn't care about that while the typescript is very strict about that. Now what if we want to make the year parameter optional? so that we can run our function with or without passing the second argument.

There are two ways to solve this issue:

  • Making the second parameter optional using syntax and then modifying the if condition:

      function taxCalculator(income: number, year?: number): number {
          if ((year || 2020) < 2020) return income * 0.2;
          return income * 0.3;
      }
    

    or

Specifying a default value for the second parameter of the function in the definition:

function taxCalculator(income: number, year = 2020): number {
    if (year < 2020) return income * 0.2;
    return income * 0.3;
}

Objects

As you know, objects in JavaScript are dynamic so that we can change their shape throughout the lifetime of the program. Now let's define an object called employee which has a property called id:

let employee = {
    id: 1111
}

Let's add another property to employee:

employee.name = "Milad";

As you know this is a valid javascript code but we are not allowed to do this in the typescript and if we do so, we get a compilation error that says: "Property 'name' does not exist on type {id: number}." If we hover the mouse pointer over employee, we can see that just like other variables we have declared so far, the typescript compiler tried to guess the type of employee variable which is an object that has a property called id of number. So we should annotate directly the type of employee variable as follows:

let employee : {
    id: number,
    name: string
} = {
    id: 1111,
};
employee.name = "Milad";

After writing the code above, we can see there is an error on employee variable. Let's hover the mouse pointer over that to see what wrong is with it:

"Property 'name' is missing in type '{id: number}' but required in type '{id: number, name: string}'"

This is because every employee should have an id and a name, but we haven't provided a name to it in its initialization step. There are two ways to fix this error:

  • Assigning an empty string to name in initialization step:

      let employee : {
          id: number,
          name: string
      } = {
          id: 1111,
          name: ""
      };
    
      employee.name = "Milad";
    
  • Making the name property optional using the typescript syntax:

      let employee : {
          id: number,
          name?: string
      } = {
          id: 1111,
      };
    
      employee.name = "Milad";
    

But as a best practice, it is better to use this code:

let employee : {
    id: number,
    name: string
} = {
    id: 1111,
    name: "Milad"
};

What if we want to make one of the properties read-only to prevent accidental modifications? To do so, the typescript provides us a feature as follows(applying read-only modifier before property name):

let employee : {
    readonly id: number,
    name: string
} = {
    id: 1111,
    name: "Milad"
};

Now if we write the following code:

employee.id = 2222;

We will get a compilation error.


Now let's add a method called retire to our employee object. To do so, we need to specify the signature of the method as follows:

let employee : {
    readonly id: number,
    name: string,
    retire: (date: Date) => void,
} = {
    id: 1111,
    name: "Milad",
    retire: (date: Date) => {console.log(date)},
};

I hope you enjoyed it, see you next time ๐Ÿ‘‹

Did you find this article valuable?

Support Milad Sadeghi by becoming a sponsor. Any amount is appreciated!

ย