Delving Deeper Into TypeScript
Going beyond the basics
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
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 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
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
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
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
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 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.
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
Now the constructor is only available within the class itself. The new keyword can’t be used outside the class to create a new
So how do we get access to our shopping cart? We use the static access method
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 —
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
protected ones are blocked off.
Changing the visibility specifier of the constructor from
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
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.
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
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:
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
Note we also changed the type of
K. Now, we can’t call our function with the invalid key of
We can, and should, also constrain
I hope you found the information in this piece informative and helpful. Good luck on your journey into the world of TypeScript.