Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

TypeScript Practical Introduction, Part 2

DZone's Guide to

TypeScript Practical Introduction, Part 2

Welcome back! In this article, we wrap up our introduction to TypeScript by going over several important features of the language, like classes and iterators.

· Web Dev Zone
Free Resource

Learn how to build modern digital experience apps with Crafter CMS. Download this eBook now. Brought to you in partnership with Crafter Software

TypeScript Classes

In TypeScript, classes are very similar to what other object-oriented programming languages provide. That is, a class is the contract definition of what is supported by the instances of this class (objects). Besides that, a class can also inherit functionality from other classes and even from interfaces (more on that in the next section). One feature that distinguishes classes in TypeScript from most of the other languages is that classes support only a single constructor. Although this might sound limiting, we will see that by supporting optional parameters, TypeScript mitigates this limitation.

To better understand these concepts, let's start creating our project management program by defining the four classes shown in the following diagram.

Defining the Entity Class

The first class that we will create is Entity. This is an abstract class that defines common characteristics that will be inherited by the other classes. To define this class, let's create a file called entity.ts in the ./src directory with the following code:

export class Entity {
  private _id: number;
  private _title: string;
  private _creationDate: Date;

  constructor(id: number, title: string) {
    this._id = id;
    this._title = title;
    this._creationDate = new Date();
  }

  get id(): number {
    return this._id;
  }

  get title(): string {
    return this._title;
  }

  set title(title: string) {
    this._title = title;
  }

  get creationDate(): Date {
    return this._creationDate;
  }
}

We start the definition of this class by using the export keyword. Exporting the class is essential so we can import it in other files, as we will see in the definition of the other classes. After that, we define three properties: _id, _title, and _creationDate. Starting properties with an underscore is important to differentiate them from their accessors (getters and setters). With the properties properly defined, we add the constructor of the class. This constructor accepts two parameters: a number to use it as the _id of the project, and a string to set in the _title property. The constructor also automatically defines the _creationDate to the current date. The last thing we do in this class definition is to add the getters and setters of the properties.

Defining the Task Class

Next, we will create Task, a concrete class that represents a task that needs to be executed. Users will be able to order tasks by priority, flag them as completed, and set a title to tasks (which will be inherited from Entity). To define this class, let's create a file called task.ts in the ./src directory with the following code:

import {Entity} from "./entity";

export class Task extends Entity {
  private _completed: boolean;
  private _priority: number;

  get completed(): boolean {
    return this._completed;
  }

  set completed(value: boolean) {
    this._completed = value;
  }

  get priority(): number {
    return this._priority;
  }

  set priority(value: number) {
    this._priority = value;
  }
}

As this class will inherit characteristics from Entity, we start this file by adding the import statement to bring the definition of Entity. After that, we define the Task class and make it extend Entity. Besides that, there is nothing too special about this class. It contains only two properties (_completed and _priority) with its accessors. Note that we don't define a constructor on Task because we will use the one inherited from Entity.

Defining the Story Class

The third class that we will create will be Story, a concrete class that represents a user story. A story can be subdivided into multiple tasks to facilitate its execution, but only one person is responsible for a story and its tasks. Besides that, a story contains a title (inherited from Entity) and a flag that identifies if the story has been completed or not. To define the Story class, let's create a file called story.ts in the ./src directory with the following code:

import {Entity} from "./entity";
import {Task} from "./task";

export class Story extends Entity {
  private _completed: boolean;
  private _responsible: string;
  private _tasks: Array<Task> = [];

  get completed(): boolean {
    return this._completed;
  }

  set completed(value: boolean) {
    this._completed = value;
  }

  get responsible(): string {
    return this._responsible;
  }

  set responsible(value: string) {
    this._responsible = value;
  }

  public addTask(task: Task) {
    this._tasks.push(task);
  }

  get tasks(): Array<Task> {
    return this._tasks;
  }

  public removeTask(task: Task): void {
    let taskPosition = this._tasks.indexOf(task);
    this._tasks.splice(taskPosition, 1);
  }
}

Just like Task, we start Story by making it extend Entity to inherit its characteristics. After that we define three properties:

  • _completed: a flag that identifies if the Story has been completed or not.
  • _responsible: a string that defines who is in charge of executing the story and its tasks.
  • _tasks: an array that contains zero or more instances of Task to be executed by the person responsible.

For the first two properties, _completed and _responsible, we define both accessors to enable their manipulation. For the _tasks property we add three methods. The first one, addTask, accepts an instance of Task to add to the array. The second one is the accessor to get all instances of Task. The third one, removeTask, receives a task to remove it from the array of tasks.

Defining the Project Class

The fourth and final class that we will create will be the Project class. A project that contains zero or more stories can be released when finished and can have a title (inherited from Entity). To define this class, let's create a file called project.ts in the ./src directory and add the following code:

import {Entity} from "./entity";
import {Story} from "./story";

export class Project extends Entity {
  private _released: boolean;
  private _stories: Array<Story>;

  get released(): boolean {
    return this._released;
  }

  set released(value: boolean) {
    this._released = value;
  }

  public addStory(story: Story) {
    this._stories.push(story);
  }

  get stories(): Array<Story> {
    return this._stories;
  }

  public removeStory(story: Story) {
    let storyPosition = this._stories.indexOf(story);
    this._stories.splice(storyPosition, 1);
  }
}

We start the definition of Project by importing Entity to inherit its characteristics. After that we define two properties: _released and _stories. The functionality provided by Project is quite similar to Story. The difference is that instead of dealing with an array of tasks, a Project deals with an array of Stories. These stories are manipulated through three methods: addStory, stories, and removeStory. The resemblance between these three methods and the ones defined on Story to deal of Tasks is big, and therefore do not require explanation.

TypeScript Interfaces

Interfaces, on TypeScript, exist to perform type checking during compile time. That is, using an interface makes the TypeScript compiler check if variables fill the contract (have the structure) defined by the interface. As occurs in other programming languages, TypeScript does not require an object to have the exact same structure as defined by the interface. To be considered valid, objects can have any shape as long as they define the functions and properties required by the interface that they implement.

For example, let's say that we want to trigger an email whenever a Task or Story is marked as completed. Instead of creating two different functions to deal with each type separately, we can define an interface to represent completable items. To practice, let's create a file called completable.ts in the ./src directory with the following source code:

export interface Completable {
  title: string;
  completed: boolean;
  completedAt?: Date;
}

After defining this interface, we can use it to restrict what objects can be passed to the function that sends emails. Let's create a file called index.ts in the ./src directory to see this in action:

import {Task} from "./task";
import {Completable} from "./completable";

function sendCompletionEmail(completable: Completable) {
  if (!completable.completed) {
    // ignore incompleted entities
    console.error(`Please, complete '${completable.title}' before sending email.`);
    return;
  }
  console.log(`Sending email about '${completable.title}'`);
  // ...
}

let bugFix = new Task(1, 'Weirdo flying bug');
sendCompletionEmail(bugFix);
bugFix.completed = true;
sendCompletionEmail(bugFix);

Note that TypeScript does not force us to explicitly implement the Completable interface. The compiler, when run, simply checks the structure of the object being passed to see if it fits the interface contract. Therefore, if we compile and run our code now, TypeScript won't display any alerts:

tsc
node ./bin/index

# > Please, complete 'Weirdo flying bug' before sending email.
# > Sending email about 'Weirdo flying bug'

Although TypeScript is flexible on that matter, it's a good practice to explicitly implement the interface. Therefore, let's update the Task class definition:

import {Entity} from "./entity";
import {Completable} from "./completable";

export class Task extends Entity implements Completable {
  // ... nothing else changes here
}

And also the definition of Story:

import {Entity} from "./entity";
import {Task} from "./task";
import {Completable} from "./completable";

export class Story extends Entity implements Completable {
  // ... nothing else changes here
}

An avid reader will notice that the Completable interface defines two properties that are not part of Task or Story. The compiler won't complain about the first property, title, because both classes inherit a property with the same name and shape from Entity. For the second property, completedAt, TypeScript won't generate alerts because the property is marked as optional through the question mark ( completedAt?: Date) added after the property name.

To learn more about interfaces on TypeScript, take a look at the official documentation.

TypeScript Decorators

Decorators offer a declarative syntax to modify the shape of classes and properties declarations. For the time being, decorators are not supported by vanilla JavaScript, but there is a proposal (currently on stage 2) to add support for them in future versions. Fortunately, as TypeScript already supports this feature, we will be able to develop elegant solutions to cross-cutting concerns like logging and transactions.

To understand how this feature works, let's say that we are interested in measuring and logging the time spent by a few functions on our program. Instead of changing the code inside all these functions, we can take advantage of decorators to decouple the performance logging from the code itself. Decorators, in the end, are just function wrappers. That is, to create a decorator, we create a function that wraps the call to the original function and change the behavior of it however we like.

To see this in action, let's create a file called log.ts in the ./src directory and add the following code:

export function Log() {
  return function (target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) {
    // 1 - keep a reference to the original function
    let originalFunction = descriptor.value || descriptor.get;

    // 2 - wrap the call to the original function to log when it
    function wrapper() {
      let startedAt = +new Date();
      let returnValue = originalFunction.apply(this);
      let endedAt = +new Date();
      console.log(`${propertyKey} executed in ${(endedAt - startedAt)} milliseconds`);
      return returnValue;
    }

    // 3 - reassigns the original function to reference the wrapper
    if (descriptor.value) descriptor.value = wrapper;
    else if (descriptor.get) descriptor.get = wrapper;
  };
}

The first step in our new Log decorator is to assign a new variable that references the original function. As we want to be able to add this decorator to methods and property accessors, we need to reference descriptor.value or descriptor.get. The latter, descriptor.get, is the reference to accessors and the former, descriptor.value, is the reference to normal methods. After that, we define our wrapper to perform the following actions:

  1. Take note of when the original function is being called.
  2. Call the original function and keep a reference to whatever is the returning value (if any).
  3. Take note of when the original function finishes its job.
  4. Log the difference between the two times so we can see how many milliseconds it took to complete the call.
  5. Return the value we got from the original function call.

The last step executed by the Log decorator declaration is to replace the original function with the wrapper. Replacing it makes the wrapper getting called whenever we reference a method/accessor decorated with @Log(), and enables us to see how long it takes the execution of the original method.

Before using the @Log() decorator, we need to instruct TypeScript to support this feature. This is done by adding the following line to tsconfig.json:

{
  "compilerOptions": {
    // ...
    "experimentalDecorators": true
  },
  // ...
}

After that, we can add @Log() to methods and accessors to measure their performance. As all methods that we have created so far are extremely fast, let's simulate a slow method and add the decorator to it. To do that, let's open the entity.ts file and make the following changes:

import {Log} from "./log";

export class Entity {
  // ...

  @Log()
  get title(): string {
    Entity.wait(1572);
    return this._title;
  }

  // ...

  private static wait(ms) {
    let start = Date.now();
    let now = start;
    while (now - start < ms) {
      now = Date.now();
    }
  }
}

As it's possible to see in the code above, the changes needed to use the decorator are simple. We just need to import it in the class that we want to probe and add the decorator to whatever methods/accessors we are interested in. The static method created, called wait, is only used to simulate slow scenarios. This method takes a number of milliseconds as parameters and halts the program execution until this time elapses.

Compiling and running the code now will produce almost the same result as before. The difference is that now we will be able to see how long our code takes to access the title property of an Entity:

tsc
node ./bin/index

# > title executed in 1572 milliseconds
# > Please, complete 'Weirdo flying bug' before sending email.
# > title executed in 1572 milliseconds
# > Sending email about 'Weirdo flying bug'

To learn more about decorators on TypeScript, take a look at the official documentation.

TypeScript Iterators

Whenever we want to loop over objects in a collection (array, map, or set) we take advantage of the iterator feature. Although TypeScript does not add anything special on top of what has been introduced on ECMAScript 2015, we will, for the sake of completeness, take a glimpse at how to use iterators. TypeScript/JavaScript provides two ways to go over objects on a collection: by using the for..of statement, and by referencing objects by its indexes.

The latter (referencing its indexes) is the classical way, probably seen by all seasoned developers:

import {Task} from "./task";

let tasks:Array<Task> = [
  new Task(1, "Buy milk"),
  new Task(2, "Buy cheese"),
  new Task(3, "Pay bills"),
  new Task(4, "Clean the house")
];

for (let i = 0; i < tasks.length; i++) {
  let task = tasks[i];
  console.log(task.title);
}

The other way, using the for..of statement, provides a more elegant way to achieve the same result:

import {Task} from "./task";

let tasks:Array<Task> = [
  new Task(1, "Buy milk"),
  new Task(2, "Buy cheese"),
  new Task(3, "Pay bills"),
  new Task(4, "Clean the house")
];

for (let task of tasks) {
  console.log(task.title);
}

As we can see, using the for..of statement makes much more sense, as we automatically get a reference to the objects in question inside the loop.

Conclusion

Although bringing type safety to JavaScript, TypeScript does it without being intrusive. That is, it helps us static checking variable types when we want, but doesn't force us to do it. Besides that, TypeScript is becoming more and more popular. Great frameworks, like Angular in the front-end and Nest in the backend, are being developed with TypeScript and IDEs are getting better at supporting this technology. Also, the community is investing time to create type definitions for well-established libraries like jQuery and lodash, which enhances their usage in this type-safe version of JavaScript. Considering all these factors, the only possible conclusion is that the future of TypeScript is very promising.

Crafter is a modern CMS platform for building modern websites and content-rich digital experiences. Download this eBook now. Brought to you in partnership with Crafter Software.

Topics:
web dev ,typescript ,web application development

Published at DZone with permission of Bruno Krebs, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}