264 lines
9.2 KiB
Markdown
264 lines
9.2 KiB
Markdown
# 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.
|
|
|
|
Let's take a look at a simple class-based example:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript-ignore
|
|
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`:
|
|
|
|
```Text
|
|
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:
|
|
|
|
```typescript-ignore
|
|
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:
|
|
|
|
```typescript-ignore
|
|
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:
|
|
|
|
```typescript-ignore
|
|
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,
|
|
|
|
```typescript-ignore
|
|
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,
|
|
|
|
```typescript-ignore
|
|
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.
|
|
|
|
```typescript-ignore
|
|
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:
|
|
|
|
```typescript-ignore
|
|
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`.
|
|
|