Harnessing TypeScript to Elevate JavaScript: Crafting Robust Web Applications

Harnessing TypeScript to Elevate JavaScript: Crafting Robust Web Applications

The Rise of JavaScript: Meeting the Needs of an Evolving Web

TL;DR: JavaScript has evolved from simple web page operations to building dynamic applications, but its weak typing can lead to errors. TypeScript enhances JavaScript by adding strong typing, improving code readability, maintainability, and error detection. By defining types and interfaces, TypeScript ensures correct object usage, reducing runtime errors. This combination allows developers to create more reliable and scalable web applications. Future articles will explore advanced TypeScript features like index signatures.

The Early Days of JavaScript

Several years after the World Wide Web came into existence, there was a need to do more than just display information on the screen. JavaScript was created to perform a small range of operations, such as allowing programmers to manipulate various aspects of the DOM.

Transition from Informational Websites to Dynamic Applications

As the online world evolved from informational websites to e-commerce platforms and web portals, JavaScript, the de facto programming language of the internet, had to expand its functionality significantly. Over the years, JavaScript has added the necessary features to meet a wide range of requirements.

Expanding JavaScript's Functionality

In recent years, plain JavaScript, even without frameworks or libraries, has been capable of building fully functional web-based applications. However, limitations such as weak typing have introduced challenges in programming with JavaScript. For instance, JavaScript lacks the ability to check for effective use of types or create new types. These features are syntactically useful, as anything provided by TypeScript is transpiled to JavaScript. Since everything in JavaScript is, in some way, an object, it makes sense to explore how TypeScript adds benefits in manipulating objects.

The Role of TypeScript in Enhancing JavaScript's Capabilities

Addressing Weak Typing with TypeScript

The basic definition of an object in JavaScript, also called a dictionary in other languages such as python, allows us to group together data. This data can be related in some way or not related at all. But the data is grouped together, nonetheless.

Practical Example of a DOM Element Object

Consider an example of an object that represents a DOM element with attributes, a text node, and child elements. This example is taken from code written to support a project that manipulates the DOM structure of a web document:

   const domType = {
        attrs: element.attributes,
        elDefnAttrs: element_defn.attrs,
        textNode: textNode,
        children: element.children
   };

Here, element is a given element retrieved from the DOM. The attrs property of the object is an array of Attr types, defined by the NamedNodeMap interface, representing all the attributes of the element. The textNode is a text node, a Node, also retrieved from the DOM. The last value retrieved from the DOM, element.children, is an HTMLCollection consisting of all the child elements attached to the referenced element. The final property in the DOMtype object is elDefnAttrs, which has as its value attr, the definition of an element used to test the DOM manipulation classes created in the project.

Benefits of TypeScript in Error Detection

As it is written above, this object used in some other block of code will not present any issues until the code is run, as JavaScript is strictly an interpreted language. Therefore, any errors introduced through the variables brought in from other parts of the code will not necessarily be noticed. However, if we define each property of the object with a type, we can identify errors in our code caused by passing the wrong values into the object. This is where TypeScript comes into play and helps us weed out any errors in this regard.

Enhancing Code Readability and Maintenance

Creating a Contract with Interfaces

Defining a type for our object will create a contract to which the object must adhere. Initializing an object declared of that type with values that don’t match will help us catch bugs and errors during transpilation. And so, these issues are less likely to occur during runtime as they would with JavaScript. The first thing we must do is define the type — here, an interface.

interface DOMtype {
    attrs: NamedNodeMap;
    elDefnAttrs: Attr[];
    children: HTMLCollection;
    textNode: Node;
}

Each property listed in this interface is given its necessary type as described above. Any code that uses an object declared from this type is much easier to maintain and read. We can know, simply by looking at the code, what the object should represent and how it might represent the DOM element as it is intended to do. This definition gives us a much better description of what you might expect a DOM element to consist. Then, in any place in our code where we need to use our DOM typed element, we can simply annotate the type as in the following example.

Annotating Types in Code

Here, we have the object defined above in its full Typescript glory, where we have implemented, through the interface, the DOMtype contract.

const domType: DOMtype = {
    attrs: element.attributes,
    elDefnAttrs: element_defn.attrs,
    textNode: textNode,
    children: element.children
};

Declaring and Initializing Objects

The const keyword indicates that we are declaring and initializing the object all at once, here. However, just as in JavaScript, we can declare and initialize the object separately, if we wish.

Guaranteeing Property Types with DOMtype Interface

Now, we have an object which guarantees that doctype.attrs consists of only a collection of Attr nodes namely the NamedNodeMap interface. Thus, this property of the domtype object can only contain all of the attributes of the element. The attributes property gives us a collection of all attribute nodes registered to the specified node, element. And, this is exactly what we’re looking to accomplish here.

Similarly, we ensure that, for each of the remaining properties, it is only possible to contain the collections, nodes, or elements by the DOMtype interface. This interface will accomplish our guarantee against providing the wrong types to an object, but it will only do so for one element that we’re representing.

However, to do so with any number of elements, we must leverage the power of Typescript to create a more complicated type. To create that more complicated type requires a more complicated explanation.

Leveraging JavaScript for Complex Types

Typing Out Each Object

Normally, in JavaScript, we’d have to type out each object as it is declared and then initialize it with values. We can alter these properties or add entirely new ones to the object as well. Regardless of how similar any given objects may be, this must be done for each one. As we’ve seen above, we can create our own types through the use of interfaces and types, which can be reused with any number of objects. Furthermore, we can alter the types we’ve previously created using the mechanisms we have at our disposal through TypeScript.

Using Computed Properties

In JavaScript, the only mechanism we have available for this is the computed property. Using a computed property, we can substitute a variable into the property of an object as we are creating it. It is defined by brackets ([]) surrounding the variable. For example, we could have the original example above:

let nodeType = "textNode";
let domElement = {
  [nodeType]: document.createTextNode("Hello, world!")
};

Generalizing Object Properties

Instead of specifying the textNode property in the example, we could use a computed type to generalize the object for the sort of node we might want to deal with:

let nodeType = "commentNode";
let domElement = {
  [nodeType]: document.createComment("This is a comment")
};

Flexibility vs. Type Safety

Then, we could specify a text node as before, or perhaps a comment node, instead:

let nodeType = "textNode";
let domElement = {
  [nodeType]: document.createTextNode("Hello, world!")
};

While this approach provides some flexibility, it doesn’t offer the same type safety benefits as TypeScript. It would be very easy to pass a variable into the object at the computed property that doesn’t reflect the role of the property that we would necessarily like to display. Instead of differentiating with which node we’d like to interact, we could pass anything else, including something outside of the DOM. In response to this concern, TypeScript has brought us the index signature, which is where we will pick up our discussion next time.

Wrapping Up: The Evolution and Future of JavaScript and TypeScript in Web Development

JavaScript has come a long way from its early days of performing simple operations on web pages. As the web has evolved into a platform for dynamic applications, JavaScript has expanded its capabilities to meet these growing demands. However, the language's inherent limitations, such as weak typing, have introduced challenges in maintaining robust and error-free code.

TypeScript has emerged as a powerful tool to address these challenges by enhancing JavaScript with strong typing and other features that improve code readability, maintainability, and error detection. By defining types and interfaces, TypeScript allows developers to create contracts that ensure the correct use of objects and their properties, reducing the likelihood of runtime errors.

As we continue to leverage JavaScript and TypeScript for building complex web applications, understanding and utilizing these tools effectively will be crucial. The ability to create and manipulate complex types, while ensuring type safety, will enable developers to build more reliable and scalable applications. The journey of JavaScript and TypeScript is a testament to the ever-evolving nature of web development, and their combined strengths will undoubtedly shape the future of the web.

Stay tuned for our next article, where we will delve deeper into the power of TypeScript's index signatures and explore more advanced concepts. Don't miss out—subscribe to our newsletter and join the conversation in the comments below!