TypeScript classes add some powerful and important features on top of traditional JavaScript classes. In this unit, we will take a closer look at class fields, access modifier keywords and more!
Class Fields
Let’s go back to our car example. In the JS world, we could have something like:
jsTry
////////////////////////////////// JavaScript, not TypeScript //////////////////////////////////classCar {constructor(make ,model ,year ) {this.make =make this.model =model this.year =year }}letsedan = newCar ("Honda", "Accord", 2017)sedan .activateTurnSignal ("left") // not safe!newCar (2017, "Honda", "Accord") // not safe!
If we stop and think for a moment, this makes sense in a world (the JS world) where
every value, including the class fields and instances of the class itself, is
effectively of type any
.
In the TypeScript world, we want some assurance that we will be stopped at compile time
from invoking the non-existent activateTurnSignal
method on our car. In order to get this
we have to provide a little more information up front:
tsTry
classCar {make : stringmodel : stringyear : numberconstructor(make : string,model : string,year : number) {this.make =make this.model =model this.year =year }}letsedan = newCar ("Honda", "Accord", 2017)Property 'activateTurnSignal' does not exist on type 'Car'.2339Property 'activateTurnSignal' does not exist on type 'Car'.sedan .("left") // not safe! activateTurnSignal newArgument of type 'number' is not assignable to parameter of type 'string'.2345Argument of type 'number' is not assignable to parameter of type 'string'.Car (2017 , "Honda", "Accord") // not safe!
Two things to notice in the code snippet above:
- We are stating the types of each class field
- We are stating the types of each constructor argument
This syntax is getting a bit verbose now — for example, the words “make”, “model” and “year” are written in four places each. As we will see below, TypeScript has a more concise way to write code like this. But first, we need to discuss the concept of access modifier keywords.
Access modifier keywords
public
, private
and protected
TypeScript provides three access modifier keywords, which can be used with class fields and methods, to describe who should be able to see and use them.
keyword | who can access |
---|---|
public |
everyone (this is the default) |
protected |
the instance itself, and subclasses |
private |
only the instance itself |
Let’s see how this works in the context of an example:
tsTry
classCar {publicmake : stringpublicmodel : stringpublicyear : numberprotectedvinNumber =generateVinNumber ()privatedoorLockCode =generateDoorLockCode ()constructor(make : string,model : string,year : number) {this.make =make this.model =model this.year =year }protectedunlockAllDoors () {unlockCar (this, this.doorLockCode )}}classSedan extendsCar {constructor(make : string,model : string,year : number) {super(make ,model ,year )this.vinNumber this.Property 'doorLockCode' is private and only accessible within class 'Car'.2341Property 'doorLockCode' is private and only accessible within class 'Car'.doorLockCode }publicunlock () {console .log ("Unlocking at " + newDate ().toISOString ())this.unlockAllDoors ()}}lets = newSedan ("Honda", "Accord", 2017)s .make Property 'vinNumber' is protected and only accessible within class 'Car' and its subclasses.2445Property 'vinNumber' is protected and only accessible within class 'Car' and its subclasses.s .vinNumber Property 'doorLockCode' is private and only accessible within class 'Car'.2341Property 'doorLockCode' is private and only accessible within class 'Car'.s .doorLockCode s .unlock ()
A couple of things to note in the example above:
- The top-level scope doesn’t seem to have access to
vinNumber
ordoorLockCode
Sedan
doesn’t have direct access to thedoorLockCode
, but it can accessvinNumber
andunlockAllDoors()
-
We see two examples of “limited exposure”
Car
can exposeprivate
functionality through defining its ownprotected
functionalitySedan
can exposeprotected
functionality through defining its ownpublic
functionality
It is important to understand that, just like any other aspect of type information, access modifier keywords
are only validated at compile time, with no real privacy or security benefits at runtime.
This means that even if we mark something as private
, if a user decides to set a breakpoint and
inspect the code that’s executing at runtime, they’ll still be able to see everything.
JS private #fields
As of TypeScript 3.8, TypeScript supports use of ECMAScript private class fields. If you have trouble getting this to work in your codebase, make sure to double-check your Babel settings
tsTry
classCar {publicmake : stringpublicmodel : string#year: numberconstructor(make : string,model : string,year : number) {this.make =make this.model =model this.#year =year }}constc = newCar ("Honda", "Accord", 2017)Property '#year' is not accessible outside class 'Car' because it has a private identifier.18013Property '#year' is not accessible outside class 'Car' because it has a private identifier.c .#year
readonly
While not strictly an access modifier keyword (because it has nothing to do with visibility),
TypeScript provides a readonly
keyword that can be used with class fields.
tsTry
classCar {publicmake : stringpublicmodel : stringpublic readonlyyear : numberconstructor(make : string,model : string,year : number) {this.make =make this.model =model this.year =year }updateYear () {this.Cannot assign to 'year' because it is a read-only property.2540Cannot assign to 'year' because it is a read-only property.++ year }}
Param properties
Ok, let’s pop a stack frame. Now that we know about access modifier keywords, let’s return to an earlier code snippet from our discussion around class fields:
tsTry
classCar {make : stringmodel : stringyear : numberconstructor(make : string,model : string,year : number) {this.make =make this.model =model this.year =year }}
TypeScript provides a more concise syntax for code like this, through the use of param properties:
tsTry
classCar {constructor(publicmake : string,publicmodel : string,publicyear : number) {}}constmyCar = newCar ("Honda", "Accord", 2017)myCar .make
This is the only time you will see an access modifier keyword next to something other than a class member. Here’s what this syntax means, conceptually:
ts
class Car {constructor(public make: string) {}}
The first argument passed to the constructor should be a
string
, and should be available within the scope of the constructor asmake
. This also creates apublic
class field onCar
calledmake
and pass it the value that was given to the constructor
It is important to understand the order in which “constructor-stuff” runs.
Here’s an example that will help us understand how this works:
tsTry
classBase {}classCar extendsBase {foo =console .log ("class field initializer")constructor(publicmake : string) {super()console .log ("custom constructor stuff")}}constc = newCar ("honda")
and the equivalent compiled output:
tsTry
"use strict";class Base {}class Car extends Base {constructor(make) {super();this.make = make;this.foo = console.log("class field initializer");console.log("custom constructor stuff");}}const c = new Car("honda");
Note the following order of what ends up in the class constructor:
super()
- param property initialization
- other class field initialization
- anything else that was in your constructor after
super()
Also note that, while it is possible in JS to put stuff before super()
,
the use of class field initializers or param properties disallows this:
tsTry
classBase {}classCar extendsBase {foo =console .log ("class field initializer")constructor(publicmake : string) {console .log ("before super")super()console .log ("custom constructor stuff")}}constc = newCar ("honda")