Introduction to classes

6 minutes read

When TypeScript was first released in 2012, it introduced several features, and one of them particularly excited developers who work with Object-Oriented Programming (OOP). This was the introduction of classes, accompanied by new related features. At that time, this was indeed a smart marketing move because developers were then able to use class features years before ES6 brought the class keyword to JavaScript in 2015. However, it's important to note that, even with ES6, neither TypeScript nor JavaScript possess a distinct class system at runtime. This means that when we refer to classes in TypeScript and ES6 (and later) JavaScript, we are actually referring to constructor functions in JavaScript, but with a syntax that aligns more with other programming languages. Let's delve deeper into what TypeScript introduced by incorporating classes into its type-safe coding environment and how it differs from JavaScript's class support.

Type inferences in class structures

Since you're familiar with JavaScript, you already know the concept of a class. To refresh our minds, classes are essentially templates for creating objects. They encapsulate the specific characteristics (properties) and behaviors (methods) defined within them. And, as you might expect, the syntax we use for working with classes in TypeScript is quite similar to JavaScript. We use the class keyword, followed by its name, and optionally, define its properties inside the constructor method, enclosed in curly brackets. Remember, if you do not provide a constructor, the language provides a default empty constructor. You can also add custom methods to your class in addition to constructor within the class:

class Garden {
    constructor(plantType, plantHeight) {
        this.plantType = plantType;
        this.plantHeight = plantHeight; // height in centimeters
    }

    plant() {
        console.log(`Planted a ${this.plantType} with an initial height of ${this.plantHeight} cm.`);
    }
}

Here we're looking at a simple Garden class example above. This code can be interpreted by both JavaScript and TypeScript, as the syntax isn't novel. Within the constructor method, the properties plantType and plantHeight are specified, but no specific values are assigned to them just yet. It's only when you create a new instance of the Garden class using the new keyword that you can assign values to these properties. Additionally, the class has a method named plant which, when called, logs a message to the console displaying the type of plant and its initial height in centimeters.

In TypeScript, we can enhance the same code with an additional layer of type safety. Let's now check out how this updated code will look in TypeScript:

class Garden {
    plantType: string;
    plantHeight: number; // height in centimeters

    constructor(plantType: string, plantHeight: number) {
        this.plantType = plantType;
        this.plantHeight = plantHeight;
    }

    plant(): void {
        console.log(`Planted a ${this.plantType} with an initial height of ${this.plantHeight} cm.`);
    }
}

As you may have noticed in the code above, here, we used type annotations to declare the data types of properties: we specified that each instance of Garden should have a plantType as a string and a plantHeight as a number; and the plant method should return void, since it does not return anything. Unlike the pure JavaScript version of the code, this helps us ensure the correct types of data will be used when creating instances of Garden, reducing potential errors.

Class inheritance

Inheritance is a foundational concept of Object-Oriented Programming (OOP). A class (the subclass or child class) can inherit properties and methods from another class (the superclass or parent class) via inheritance. Objects can also inherit from classes, so it acquires properties and methods of its parent class. Building inheritance relationships helps use the existing class or object templates again and shows which comes from which.

TypeScript, like JavaScript and many other OOP languages, uses the extends keyword to establish inheritance between classes. Additionally, in a derived class's constructor, before you add new properties or methods, you must call the parent class's constructor using super(). (You can think of it like laying the foundation before building a house.) Once a class inherits from another class, it gains access to the properties and methods of the base class, and it can also introduce its own or override the inherited ones.

Let's take a look at the following example:

class Flower extends Plant {
    color: string;

    constructor(name: string, color: string) {
        super(name);
        this.color = color;
    }

    describeColor(): void {
        console.log(`This flower is ${this.color}.`);
    }
}

let rose = new Flower("Rose", "red");
rose.describe();       // Output: This is a Rose.
rose.describeColor();  // Output: This flower is red.

In the code above, the Flower class inherits from the Plant class. Thanks to inheritance, an instance of the Flower class can access both the describe method from the Plant class and its own describeColor method.

Classes vs. interfaces

When diving into TypeScript, one might wonder about the distinction between classes and interfaces, especially since they seem to play similar roles in defining the shape of objects. However, they serve different purposes:

  • Classes are simply templates for creating objects and incorporate both properties and methods, and they exist in the compiled JavaScript. That means, when you write TypeScript code, classes are used to generate JavaScript code during compilation, and it affects how your code behaves at runtime.

  • Interfaces are solely used by TypeScript as a way to type-check the shape of an object. In other words, with interfaces, we can specify the expected shape of an object, including its properties and their types.

  • Interfaces don't translate to any JavaScript code and have no runtime representation.

To understand better, let's see this example:

interface PlantInterface {
    name: string;
    describe(): void;
}

class Plant implements PlantInterface {
    name: string;

    constructor(name: string) {
        this.name = name;
    }

    describe(): void {
        console.log(`This is a ${this.name}.`);
    }
}

In this code, we have an interface named PlantInterface that defines the structure objects should have, specifying a name property and a describe method. The Plant class implements this interface, which means it must have these properties and methods. When we create an instance of the Plant class, it sticks to the structure defined by the interface. Basically, the interface acts as a sort of specification for how objects should look, while the class creates objects that comply with that specification, ensuring consistency and type-checking in the code.

Conclusion

TypeScript's class system and its use of interfaces are important features to understand for anyone getting started with TypeScript. Classes give a clear way to set up objects, while interfaces help ensure these objects have the right format. This helps reduce mistakes related to wrong data types. When you use classes and interfaces together, you get tools that help make your coding more reliable and safe from common errors. For those new to TypeScript, using both classes and interfaces can be a big help in ensuring your code works well.

9 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo