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:
false
TypeScript class fields are public by default: they're accessible from outside the class. In the class above, the
nameandvaccinatedfields are both public.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
new Cat('Ms. Fluff', true).name;Result:
'Ms. Fluff'
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
privatekeyword 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 likethis.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'
- 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.
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,
Cattries to change the return type of a method that it inherited fromPet. 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'.Two things to note about that example. First, our
Catclass didn't need to re-specify thenameandvaccinatedfields, or the constructor. All of that was inherited from thePetclass.Second, the type error happens because the
needsVaccinationmethod has a different return type:Pet'sneedsVaccinationreturns abooleanbutCat's returns astring. 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 aPetshould also work with aCat.If
needsVaccinationreturns a boolean, we can do things like invert it via!pet.needsVaccination(). But if TypeScript allowedCatto overloadneedsVaccinationto return'yes'or'no', then!pet.needsVaccination()would no longer make sense. The problem is that!'yes'and!'no'are bothfalse. When the pet in question is a cat,!pet.needsVaccination()would always befalse, 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
Albumclass that:- Has two properties,
nameandcopiesSold. - 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]
- Has two properties,