Delving Deeper Into TypeScript

Going beyond the basics

Richard Prasad
Jan 28 · 11 min read
Photo by Daniel Jerez on Unsplash

For JavaScript programmers who’ve already taken the plunge and learned the basics of TypeScript, this piece will show you more advanced usages of the language. We’ll also take a brief look at two important concepts every object-oriented programmer should know about: SOLID and design patterns.

For those new to TypeScript or those needing a refresher, you can check out my other piece, “Going from JavaScript to TypeScript.”


Abstract Classes

In JavaScript, there’s only type of class, but in TypeScript there are two types. The first type is a regular class and is functionally the same as a JavaScript class. The second type is called an abstract class and is used to define a base class.

A base class is used to define a set of core properties and methods that all child classes will inherit. We can create a base class with an ordinary class as well; however, when we make our base class abstract, we can also specify that certain properties and methods must be defined by the child class. We’ll see what this means and how it works shortly.

An abstract class cannot be instantiated — i.e., you can’t create an instance from one:

Let’s add some properties to this class:

We will discuss what protected means later. Our first property, PI, is a regular property. It is also marked as readonly because we don’t want its value to ever change.

Our second property, calcType, is an abstract property. Marking a property as abstract means a child class of our base class must define this property. Let’s create a child class of Calculator called SimpleCalculator.

We have an error, which we can remedy by clicking on “Quick Fix,” then “Implement inherited abstract class.”

Let’s also provide a value for the calcType:

Since calcType is an abstract property, we’re required to define it in the child class. Also, notice we drop the abstract keyword from the calcType in the child class. Let’s create two more child classes from Calculator:

By forcing child classes to provide their own definition for the calcType, we can ensure each child class is configured properly. Our child classes can vary in configuration and behavior independently from our base class. We don’t have to modify our base class to account for every possible type of child class that comes up in the lifetime of an application.


SOLID and the Open-Closed Principle

In the world of object-oriented programming, you’ll eventually encounter a set of design principles known as SOLID. There are five parts to SOLID, with the “O” standing for the open-closed principle. Our classes are “open for extension, but closed for modification.”

Following the SOLID principles allows us to write code that’s easier to understand, enhance, and maintain. Once we’ve fleshed out a class, we’ll no longer modify it. Imagine if years from now, dozens of other classes depended on a particular class, and a new programmer made a change to that class such that it broke every other class that depended on it?


The Protected Modifier and an Abstract Method

Creating a child class from another class is called inheritance. Unlike JavaScript, TypeScript provides a feature known as visibility modifiers, which allow you to control which properties a child class will inherit and which will remain only accessible within the parent (base) class.

A property or method marked as private isn’t inherited, while one marked as protected is. Those marked as public are also inherited. Generally, our methods are public, and our properties are protected. Let’s add two methods to our Calculator class: one regular and one abstract.

Go ahead and fix all the new errors by selecting “Implement inherited abstract class for each child class.”

All of our child classes inherit a sum method, which is defined in our base class Calculator, but each child class must provide its own implementation for the mul (multiply) method.

While this example is contrived, let’s delete the GraphingCalculator class and provide two different implementations for mul:

SimpleCalculator is a primitive device and can only multiply by adding repeatedly. ScientificCalculator, on the other hand, has built-in circuitry for performing multiplication. Both subclasses share the already defined sum method from the Calculator.


Collections of Calculators and Constraining Generics

Take a look at the following code snippet:

We see instances of our child classes are of the same type as our base class. This means we can treat child-class instances as if they’re all of the same type. Let’s keep this in mind.

Calculator shopping cart

Imagine we run an online store that sells calculators. When a customer visits our site, they have a shopping cart available to them for ordering calculators. Our shopping cart stores each calculator ordered in an array:

What type annotation do we give to calcOrder? We can modify the class to accept a generic parameter of type T, but T could represent any type:

What we want to do is constrain our generic parameter:

The code fragment T extends SimpleCalculator | ScientificCalculator uses a union type to constrain T to be either one of two types, but what if we have three, four, or a 100 different types of calculators? It’d be too much work to list out every possible type. Not only that, but we’d have to constantly modify our base-class definition, which goes against the open-closed principle.

Fortunately, since all our child-calculator classes derive from the same base class and we’ve shown earlier that every child class instance is also of the same type as our base class, we can shorten our constraint as follows:


Design Patterns and the Singleton Pattern

Another concept you’ll encounter in object-oriented programming is that of design patterns. Design patterns are a series of solutions to commonly occurring programming problems. There are a few dozen of them, and they are essential to understand as they’re frequently used in the construction of APIs and frameworks. Design patterns are also how we obey the “O” in SOLID.

The Singleton pattern allows us to restrict a class to having only a single instance created from it. A special method is provided to give access to this one instance.

A shopping cart is a good candidate for becoming a singleton: We only want one instance of a shopping cart in our (front-end) application. No matter where we are in our application’s code, we can be sure we’re only accessing and modifying a single cart.

Let’s change our ShoppingCart class into a singleton. There are three steps involved.

Create a self-referencing property

A reference to the one and only instance of our shopping cart is stored in the class. This reference is a static property, meaning it resides within the class itself rather than any instance of it.

(Static properties — a.k.a. static fields — are currently experimental in JavaScript. Static methods have been available since ES6. Click here for more information.)

Note that static properties can’t take a variable generic type. We must specify one — in this case, Calculator. We’re also allowing this reference to be null, and we’ll see why soon.

Provide a static access point

This access point is a static method, which means it, too, can’t take (or return) a variable generic type. getCart is the only way to create an instance of our shopping cart.

getCart checks to see if we have already created a shopping cart. At first, our cart reference is null. getCart then invokes the constructor and sets the static property cart to this new instance. The if statement will block anyone from ever being able to create another instance of our shopping cart. Each subsequent call to getCart will always return the same cart instance.

Block off the constructor

The final step is to prevent anyone from using the new operator with our ShoppingCart class. We do this by making the constructor private:

Now the constructor is only available within the class itself. The new keyword can’t be used outside the class to create a new ShoppingCart instance:

So how do we get access to our shopping cart? We use the static access method getCart:

Now our application will only have a single copy of a shopping cart for each user no matter where in the code base we are. Additionally, our shopping cart will only accept items that are calculators.

A quick aside on visibility modifiers

Not only do the visibility modifers — public, private, and protected — control how inheritance works from the parent (base) class to the child class, they also specify whether or not properties and methods can be accessed from outside the class. public properties and methods are open to the outside world, while private and protected ones are blocked off.

Changing the visibility specifier of the constructor from public to private restricts access to the constructor to solely within the class. The new keyword can no longer be used from the outside to create a ShoppingCart instance.


Function (and Method) Overloading

Other object-oriented languages, such as C++ and Java, allow you to write multiple functions with the same name but with a different set of parameters:

The code above is Java. We’ll write a TypeScript version, but the concept of function overloading is clearer in Java than it is in TypeScript.

We have four versions of a function called greet. They vary from each other by the number and types of their parameters — i.e., their signatures are different.

The main function, which is where our application begins executing code, calls greet four times. Java will match the number of arguments and their types to figure out which greet function to use.

We’re also able to essentially reuse code from one version of a function inside another by composing them together in a chain: The greet function that takes two strings calls the version that takes one string, which in turn calls the version with no arguments, which in turn prints the word Hello. The overloaded functions themselves don’t have to contain the code to print Hello.

Unfortunately, TypeScript doesn’t have this elegant and straightforward version of function overloading:

This code (mostly) does the same thing as the Java version but is obviously not pretty to look at. However, you’ll encounter code like this when looking through the source code of your favorite third-party packages, so it’s a good idea to be familiar with it.

We define the body for only one version of our function. This version is the most general form in terms of the parameters it accepts. See the function on line 155 in the above example.

param1 is a required parameter, so all overloaded versions must provide at least one argument. The possible argument types for param1 for any given overload must be one of the types specified in the general form: number, string, or undefined.

The second argument, param2, is marked as optional, thus we can create overloads with either one or two arguments. If we do create an overload with a second parameter, it can only be of the type string; see line 151.

While we don’t have a version that can take zero arguments, we can create a functional equivalent by allowing param1 to be undefined.

The logic inside the most generic version (line 155) is designed to account for all the combinations of argument counts and types described by each overload.

A quick aside on function versus method overloading

When functions are part of a class, they’re known as methods. When we talk about overloading functions inside a class — e.g., methods — we call it method overloading. All of our examples have been of method overloading.


Generics and Object Keys

The final topic we’ll cover in this piece is dealing with generics and object keys. Let’s create a function that takes in an array of objects as well as some key and then prints out the value for the given key for each object. We’ll also provide an array of objects:

Now let’s invoke printValues, once with a valid key and once with an invalid one:

We can ensure our function is never called with an invalid key by specifying a second generic parameter, K, and constraining it such that it’s limited to the keys of the first generic parameter T.

Note we also changed the type of key from string to K. Now, we can’t call our function with the invalid key of lastName:

We can, and should, also constrain T :


Conclusion

I hope you found the information in this piece informative and helpful. Good luck on your journey into the world of TypeScript.

Better Programming

Advice for programmers.

Richard Prasad

Written by

Full-stack web developer

Better Programming

Advice for programmers.

More From Medium

More from Better Programming

More from Better Programming

More from Better Programming

More from Better Programming

Why Do Incompetent Managers Get Promoted?

1.97K

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade