Execute Program

Everyday TypeScript: Classes

Welcome to the Classes lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • TypeScript allows us to statically type JavaScript classes. In this TypeScript course, we'll assume that you have a basic familiarity with classes in JavaScript, including constructors, instance methods, and static methods. If JavaScript's classes are new to you, we recommend reviewing the lessons on classes in our Modern JavaScript course.

  • Classes have fields (data) and methods (functions), both of which need static types. Fortunately, fields use syntax similar to variables, and methods use syntax similar to functions.

  • Let's begin with a simple class that we'll use for a few examples:

  • >
    class Cat {
    name: string;
    vaccinated: boolean;

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

    needsVaccination(): boolean {
    return !this.vaccinated;
    }
    }
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    new Cat('Ms. Fluff', true).needsVaccination();
    Result:
    falsePass Icon
  • TypeScript class fields are public by default: they're accessible from outside the class. In the class above, the name and vaccinated fields are both public.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    new Cat('Ms. Fluff', true).name;
    Result:
    'Ms. Fluff'Pass Icon
  • We can make fields private if we want to. A private field isn't accessible from outside the class; it can only be accessed by the class's own methods.

  • For historical reasons, there are two ways to make TypeScript fields private. The TypeScript team wanted to support private fields in TypeScript from the beginning, but at the time JavaScript had no way to define private fields. TypeScript added a private keyword for that purpose. That keyword only works in TypeScript.

  • Today, JavaScript has its own syntax for private fields. Of course, TypeScript also supports that new JavaScript syntax. We'll focus on the new syntax for two reasons. First, it works in both JavaScript and TypeScript. Second, the new syntax also hides the private fields at runtime, adding a small additional layer of protection.

  • Private fields look like this.#name, with the hash included, whereas public fields look like this.name. Code outside the class can't access a private field. That's a type error.

  • >
    class Cat {
    name: string;
    #vaccinated: boolean;

    constructor(name: string, vaccinated: boolean) {
    this.name = name;
    this.#vaccinated = vaccinated;
    }

    needsVaccination(): boolean {
    return !this.#vaccinated;
    }
    }
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    new Cat('Ms. Fluff', true).name;
    Result:
    'Ms. Fluff'Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    new Cat('Keanu', true).#vaccinated;
    Result:
    type error: Property '#vaccinated' is not accessible outside class 'Cat' because it has a private identifier.Pass Icon
  • Other code tends to rely on public fields, so it's important to make careful decisions between public and private. If we make a field public, but later remove it, rename it, or split it into multiple simpler fields, then code that relies on the original field will fail. That makes our classes harder to change.

  • TypeScript classes can extend other classes, just like they can in JavaScript. (In addition to "extend a class", we sometimes say "subclass a class" or "inherit from a class". These all mean the same thing in JavaScript and TypeScript.)

  • TypeScript's subclass syntax matches JavaScript's: class TheChildClass extends TheParentClass { ... }. The TypeScript compiler also ensures that the child class's types are compatible with the parent.

  • In the next example, Cat tries to change the return type of a method that it inherited from Pet. This causes a type error.

  • >
    class Pet {
    name: string;
    vaccinated: boolean;

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

    needsVaccination(): boolean {
    return !this.vaccinated;
    }
    }

    class Cat extends Pet {
    needsVaccination(): string {
    return this.vaccinated ? 'no' : 'yes';
    }
    }

    new Cat('Ms. Fluff', true);
    Result:
    type error: Property 'needsVaccination' in type 'Cat' is not assignable to the same property in base type 'Pet'.
      Type '() => string' is not assignable to type '() => boolean'.
        Type 'string' is not assignable to type 'boolean'.Pass Icon
  • Two things to note about that example. First, our Cat class didn't need to re-specify the name and vaccinated fields, or the constructor. All of that was inherited from the Pet class.

  • Second, the type error happens because the needsVaccination method has a different return type: Pet's needsVaccination returns a boolean but Cat's returns a string. If TypeScript allowed that type change, it would break an important part of inheritance: substitutability. By "substitutability", we mean that any code that works with a Pet should also work with a Cat.

  • If needsVaccination returns a boolean, we can do things like invert it via !pet.needsVaccination(). But if TypeScript allowed Cat to overload needsVaccination to return 'yes' or 'no', then !pet.needsVaccination() would no longer make sense. The problem is that !'yes' and !'no' are both false. When the pet in question is a cat, !pet.needsVaccination() would always be false, which is wrong.

  • In JavaScript, this mistake can make it all the way to production, causing runtime failures or, worse, incorrect data written to a database. TypeScript prevents that confusion by ensuring that child classes' method signatures are compatible with the parent class's signatures. The mistake is caught at compile time and the bug never happens.

  • Here's a code problem:

    Write an Album class that:

    • Has two properties, name and copiesSold.
    • Takes both of those properties as constructor arguments.
    • Defines a method, platinum, that tells us whether the record has been "certified platinum". (A "platinum" record is one that sold at least 1,000,000 copies.)
    class Album {
    name: string;
    copiesSold: number;
    constructor(name: string, copiesSold: number) {
    this.name = name;
    this.copiesSold = copiesSold;
    }
    platinum() {
    return this.copiesSold >= 1000000;
    }
    }
    const loveSupreme = new Album('A Love Supreme', 500000);
    const kindOfBlue = new Album('Kind of Blue', 4490000);
    [
    loveSupreme.name,
    loveSupreme.copiesSold,
    loveSupreme.platinum(),
    kindOfBlue.platinum(),
    ];
    Goal:
    ['A Love Supreme', 500000, false, true]
    Yours:
    ['A Love Supreme', 500000, false, true]Pass Icon