Classes and Type Annotations

Let's learn about classes and type annotations in this lesson.

We'll cover the following

TypeScript takes full advantage of the class features added to JavaScript in ES6.

There are a lot of references for ES6 classes; http://es6-features.org has the basic syntax.

The goal of the TypeScript extensions to class syntax is to allow the TypeScript compiler to treat object attribute references and method calls the way functions and assignments are treated. We want to be able to tell from the class of the instance what attributes exist and the expected type of those attributes.

In TypeScript, any method defined in a class is available to instances of the class. You can annotate arguments and return values of a method just as you would for functions.

The first real change in TypeScript classes compared to JavaScript is the need to explicitly list attributes of the class that would, in plain JavaScript, only be created when assigned. In TypeScript, if you are going to refer to an attribute like this.color = "red", the existence of the color attribute needs to have already been declared.

Again, we’ve already seen this at the beginning of our GenericToggle class:

export default class GenericToggle {
  toggleButtons: NodeListOf<HTMLElement>
  targets: NodeListOf<HTMLElement>
  hidden: boolean
  /* and so on */
}

We are specifying that instances of GenericToggle have three properties: toggleButtons, targets, and hidden. This information gets used in a couple of different ways.

The name GenericToggle can be used as a type just like the basic types we have already covered. You can annotate a variable, function parameter, or return value to be of type GenericToggle, and the compiler will require that those instances use only properties that have been explicitly defined by the GenericToggle class.

TypeScript expands upon the ECMA Script class syntax in that it allows you to avoid repeating a declaration of a property, the naming of the property in the list of constructor parameters, and the assigning of the property.

So, instead of:

class User {
  firstName: string
  lastName: string
  
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}

You can use a modifier in the constructor parameter list, most commonly, private:

class User {
  constructor(private firstName: string, private lastName: string) {
  }
}

The two examples are functionally equivalent. When the constructor is called, TypeScript sets the properties of the instance based on the arguments to the constructor. In addition to the private, protected, and public keywords already existing in ES6, TypeScript adds readonly. A readonly property must either be set literally when it is declared or set in the constructor.

Defining interfaces

Sometimes your type checking only cares about a subset of the properties of an object, or you have a common set of properties that might be shared by a number of different objects that are otherwise unrelated. It’s useful to allow the type checking system to be as specific as possible and to specify that the objects being used are restricted to only the properties that are being used in a specific context.

Alternately, you might have some data in your system, such as the result of a server call that has returned JSON objects, and you’d like to assert in the type system that the certain properties must exist in the data. But you don’t necessarily need to declare a class, because there might not be any behavior for that data, just a structure that you want to use.

You can manage all these issues in TypeScript by using interfaces. An interface is just a list of properties that go together. Our GenericToggle class, for example, might be a specific case of an interface that has buttons and targets:

interface Actor {
  toggleButtons: NodeListOf<HTMLElement>
  targets: NodeListOf<HTMLElement>
}

We’ve also seen this in our React code, where we have been using interfaces to define type information for the props objects passed to each component.

Properties don’t have to be just variables, they can also be methods, like onFilterButtonClick(x: Event): void. There are more complex scenarios for some rarer items that we won’t get into here. As with functions, you can specify a property is optional by replacing the : with ?:. I recommend doing that rarely, as it weakens the type checking.

Once we have this interface, it’s just as much a type as any basic type or class. We can specify the interface as the type of a variable, parameter, or return value, and the TypeScript compiler will enforce that those variables only use the listed properties.

You can also specify that a class implements a particular interface, so we could now say:

export default class GenericToggle implements Actor {
  toggleButtons: NodeListOf<HTMLElement>
  targets: NodeListOf<HTMLElement>
  hidden: boolean
  /* and so on */
}

Note that we still have to actually declare the properties in the class even though they are already declared in the interface. In practice, what’s happening here is that the TypeScript compiler will require the class to declare all the properties of the interface. That’s less helpful than you might think.

Interfaces, like classes, can use the extends keyword to mean “everything that another interface has, plus more”:

interface StatefulActor extends Actor {
  state: boolean
}

Our new StatefulActor interface still includes the targetButton and targets from the Actor interface, but also includes state.

Classes and interfaces can extend each other, but I’d recommend trying to keep classes extending other classes and interfaces extending other interfaces to avoid confusion.

Get hands-on with 1200+ tech skills courses.