9.4 KiB
Classes
Traditional JavaScript focuses on functions and prototype-based inheritance as the basic means of building up reusable components, but this may feel a bit awkward to programmers more comfortable with an object-oriented approach, where classes inherit functionality and objects are built from these classes.
Starting with ECMAScript 2015, also known as ECMAScript 6, JavaScript programmers will be able to build their applications using this object-oriented class-based approach. TypeScript, allows you to use these techniques now, compiling them down to JavaScript that works across all major browsers and platforms, without having to wait for the next version of JavaScript.
Let's take a look at a simple class-based example:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
We declare a new class Greeter
. This class has three members: a property called greeting
, a constructor, and a method greet
.
You'll notice that in the class when we refer to one of the members of the class we prepend this.
.
This denotes that it's a member access.
In the last line we construct an instance of the Greeter
class using new
.
This calls into the constructor we defined earlier, creating a new object with the Greeter
shape, and running the constructor to initialize it.
Inheritance
In TypeScript, we can use common object-oriented patterns. Of course, one of the most fundamental patterns in class-based programming is being able to extend existing classes to create new ones using inheritance.
Let's take a look at an example:
class Animal {
name: string;
constructor(theName: string) { this.name = theName; }
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
class Horse extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}
let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");
sam.move();
tom.move(34);
This example covers quite a few of the inheritance features in TypeScript that are common to other languages.
Here we see the extends
keywords used to create a subclass.
You can see this where Horse
and Snake
subclass the base class Animal
and gain access to its features.
Derived classes that contain constructor functions must call super()
which will execute the constructor function on the base class.
The example also shows how to override methods in the base class with methods that are specialized for the subclass.
Here both Snake
and Horse
create a move
method that overrides the move
from Animal
, giving it functionality specific to each class.
Note that even though tom
is declared as an Animal
, since its value is a Horse
, when tom.move(34)
calls the overriding method in Horse
:
Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.
Public, private, and protected modifiers
Public by default
In our examples, we've been able to freely access the members that we declared throughout our programs.
If you're familiar with classes in other languages, you may have noticed in the above examples
we haven't had to use the word public
to accomplish this; for instance,
C# requires that each member be explicitly labeled public
to be visible.
In TypeScript, each member is public
by default.
You may still mark a member public
explicitly.
We could have written the Animal
class from the previous section in the following way:
class Animal {
public name: string;
public constructor(theName: string) { this.name = theName; }
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
Understanding private
When a member is marked private
, it cannot be accessed from outside of its containing class. For example:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // Error: 'name' is private;
TypeScript is a structural type system. When we compare two different types, regardless of where they came from, if the types of all members are compatible, then we say the types themselves are compatible.
However, when comparing types that have private
and protected
members, we treat these types differently.
For two types to be considered compatible, if one of them has a private
member,
then the other must have a private
member that originated in the same declaration.
The same applies to protected
members.
Let's look at an example to better see how this plays out in practice:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
constructor() { super("Rhino"); }
}
class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}
let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");
animal = rhino;
animal = employee; // Error: 'Animal' and 'Employee' are not compatible
In this example, we have an Animal
and a Rhino
, with Rhino
being a subclass of Animal
.
We also have a new class Employee
that looks identical to Animal
in terms of shape.
We create some instances of these classes and then try to assign them to each other to see what will happen.
Because Animal
and Rhino
share the private
side of their shape from the same declaration of
private name: string
in Animal
, they are compatible. However, this is not the case for Employee
.
When we try to assign from an Employee
to Animal
we get an error that these types are not compatible.
Even though Employee
also has a private
member called name
, it's not the one we declared in Animal
.
Understanding protected
The protected
modifier acts much like the private
modifier with the exception that members
declared protected
can also be accessed by instances of deriving classes. For example,
class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // error
Notice that while we can't use name
from outside of Person
,
we can still use it from within an instance method of Employee
because Employee
derives from Person
.
A constructor may also be marked protected
.
This means that the class cannot be instantiated outside of its containing class, but can be extended. For example,
class Person {
protected name: string;
protected constructor(theName: string) { this.name = theName; }
}
// Employee can extend Person
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // Error: The 'Person' constructor is protected
Readonly modifier
You can make properties readonly by using the readonly
keyword.
Readonly properties must be initialized at their declaration or in the constructor.
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // error! name is readonly.
Parameter properties
In our last example, we had to declare a readonly member name
and a constructor parameter theName
in the Octopus
class, and we then immediately set name
to theName
.
This turns out to be a very common practice.
Parameter properties let you create and initialize a member in one place.
Here's a further revision of the previous Octopus
class using a parameter property:
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {
}
}
Notice how we dropped theName
altogether and just use the shortened readonly name: string
parameter on the constructor to create and initialize the name
member.
We've consolidated the declarations and assignment into one location.
Parameter properties are declared by prefixing a constructor parameter with an accessibility modifier or readonly
, or both.
Using private
for a parameter property declares and initializes a private member; likewise, the same is done for public
, protected
, and readonly
.