Модификаторы доступа
public
по умолчанию
В TypeScript же, каждый член класса будетpublic
по умолчанию.
Но мы можем пометить члены классаpublic
явно. Пример:
class Animal {
public name: string;
public constructor(theName: string) { this.name = theName; }
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
private
Когда член класса помечен модификатором private
, он не может быть доступен вне этого класса. Например:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // ошибка: 'name' is private;
TypeScript — это структурная система типов. Когда мы сравниваем два разных типа, независимо от того где и как они описаны и реализованы, если типы всех их членов совместимы, можно утверждать, что и сами типы совместимы. Впрочем, когда сравниваются типы с модификатором доступа private, это происходит по-другому. Два типа будут считаться совместимыми, если оба члена имеют модификатор private из того же самого объявления. Это относится и к protected членам.
Давайте посмотрим пример, чтобы понять принцип работы на практике:
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; // ошибка: 'Animal' and 'Employee' are not compatible
В этом примере у нас есть классы Animal и Rhino, где Rhino является подклассом Animal. У нас также есть новый класс Employee, который выглядит идентично Animal. Мы создаем экземпляры этих классов и пытаемся получить доступ к каждому, чтобы посмотреть что произойдет. Поскольку private часть Animal и Rhino объявлена в одном и том же объявлении, они совместимы. Тем не менее, это не относится к Employee. Когда мы пытаемся присвоить Employee к Animal, мы получаем ошибку: эти типы не совместимы. Несмотря на то, что Employee имеет private член под именем name, это не тот член, который мы объявили в Animal.
protected
Модификатор protected
действует аналогично private
за исключением того, что члены, объявленные protected
, могут быть доступны в подклассах. Например:
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); // ошибка
Обратите внимание на то, что мы не можем использовать членname
вне классаPerson
, но можем использовать внутри метода подклассаEmployee
, потому чтоEmployee
происходит отPerson
.
Конструктор тоже может иметь модификаторprotected
. Это означает, что класс не может быть создан за пределами содержащего его класса, но может быть наследован. Например:
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"); // ошибка: The 'Person' constructor is protected
Модификаторreadonly
Вы можете делать свойства доступными только для чтения с помощью ключевого словаreadonly.
Свойства, доступные только для чтения, должны быть инициализированы при их объявлении или в конструкторе.
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"; // ошибка! name is readonly.
Аксессоры (геттеры/сеттеры)
TypeScript поддерживает геттеры и сеттеры как способ перехвата обращений к свойствам объекта. Это дает вам больший контроль над моментом взаимодействия со свойствами объектов.
Давайте перепишем простой класс с использованиемget
иset
. Для начала запишем пример без использования геттеров и сеттеров.
class Employee {
fullName: string;
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}
Разрешать напрямую устанавливатьfullName
- довольно удобно, но это может привести к проблемам если кто-то захочет изменить имя по своему желанию.
В этой версии мы проверяем наличие у пользователя секретного пароля, перед тем как позволить ему внести изменения. Мы делаем это заменяя прямой доступ кfullName
и используем сеттерset
, который проверяет пароль. Кроме того, добавляем соответствующийget
, чтобы код работал так же, как и в предыдущем примере.
let passcode = "secret passcode";
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "secret passcode") {
this._fullName = newName;
}
else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}
Чтобы убедиться, что наш метод доступа проверяет пароль, мы можем модифицировать его и увидеть, что при несовпадении мы получаем сообщение о том, что не можем модифицировать объект работника.
Внимание: аксессоры требуют установки в компиляторе генерации кода по стандарту ECMAScript 5 или выше.
Статические свойства
До сих пор мы говорили только об членах экземпляра класса, тех, которые появляются в объекте, когда он инициализирован. Но мы можем создавать и статические члены класса, те, которые видны в классе без создания экземпляра. В этом примере мы используем static
, так как origin
— это общее значение для всех объектов. Каждый экземпляр получает доступ к этому значению, предваряя его именем класса. Схоже с тем, как мы добавляем this. для доступа к членам экземпляра, для доступа к статическим членам используется Grid.
.
class Grid {
static origin = {x: 0, y: 0};
calculateDistanceFromOrigin(point: {x: number; y: number;}) {
let xDist = (point.x - Grid.origin.x);
let yDist = (point.y - Grid.origin.y);
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor (public scale: number) { }
}
let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scale
console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
Абстрактные классы
Абстрактные классы — это базовые классы, от которых наследуются другие. Их экземпляры не могут быть созданы напрямую. В отличие от интерфейса, абстрактный класс может содержать детали реализации своих членов. Ключевое словоabstract
используется для определения абстрактных классов, а также абстрактных методов в рамках таких классов.
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log("roaming the earth...");
}
}
Методы в рамках абстрактного класса, помеченные как абстрактные, не содержат реализацию и должны быть реализованы в производных классах. Синтаксис у абстрактных методов — такой же, как у методов интерфейса. Оба определяют сигнатуру метода, не описывая его тело. Описание абстрактного метода должно содержать ключевое словоabstract
, а также может содержать модификаторы доступа.
abstract class Department {
constructor(public name: string) {
}
printName(): void {
console.log("Department name: " + this.name);
}
abstract printMeeting(): void; // должен быть реализован в производном классе
}
class AccountingDepartment extends Department {
constructor() {
super("Accounting and Auditing"); // конструкторы в производных классах должны вызывать super()
}
printMeeting(): void {
console.log("The Accounting Department meets each Monday at 10am.");
}
generateReports(): void {
console.log("Generating accounting reports...");
}
}
let department: Department; // окей, создана ссылка на абстрактный класс
department = new Department(); // ошибка: cannot create an instance of an abstract class
department = new AccountingDepartment(); // окей, создан и присвоен не абстрактный класс
department.printName();
department.printMeeting();
department.generateReports(); // ошибка: method doesn't exist on declared abstract type
Дополнительные методы
Когда вы объявляете класс в TypeScript, вы фактически создаете несколько объявлений одновременно. Первое объявление — тип экземпляра класса.
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet());
В данном случае, когда мы говоримlet greeter: Greeter
, мы используемGreeter
как тип экземпляров классаGreeter
. Это почти привычка программистов из других объектно-ориентированных языков программирования.
Мы также создаем еще одно значение, которое называетсяфункцией-конструктором. Эта функция вызывается, когда мы создаем экземпляры класса с помощьюnew. Чтобы посмотреть, как это выглядит на практике, давайте посмотрим на код JavaScript, сгенерированный компилятором из примера выше:
let Greeter = (function () {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
return Greeter;
})();
let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());
Здесь let Greeter
присваивается функция-конструктор. Когда мы указываем new
и запускаем эту функцию, мы получаем экземпляр класса. Функция-конструктор также содержит все статические члены класса. Другой способ думать о каждом классе: есть часть экземпляр и статическая часть.
Давайте изменим немного код, чтобы показать эту разницу:
class Greeter {
static standardGreeting = "Hello, there";
greeting: string;
greet() {
if (this.greeting) {
return "Hello, " + this.greeting;
}
else {
return Greeter.standardGreeting;
}
}
}
let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";
let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());
В этом примереgreeter1
работает аналогично тому, что выше. Мы создали экземпляр классаGreeter
и используем объект. Это мы уже видели.
Дальше используем непосредственно класс. Создаем новую переменную с именемgreeterMaker
. Эта переменная будет содержать сам класс, или, другими словами, функцию-конструктор. Здесь мы используемtypeof Greeter
, это выглядит как "дайте мне тип самого классаGreeter
", а не экземпляра. Или, точнее, "дайте мне тип идентификатора, что зоветсяGreeter
", который является типом функции-конструктора. Этот тип будет содержать все статические членыGreeter
, вместе с конструктором, который создает экземпляры классаGreeter
. Мы продемонстрировали это, использовавnew
сgreeterMaker
, создавая новые экземплярыGreeter
и вызывая их, как раньше.
Использование класса в качестве интерфейса
Как мы уже говорили в предыдущем разделе, объявление класса создает две вещи: тип, описывающий экземпляры класса, и функцию-конструктор. Так как классы создают типы, мы можем использовать так же, как интерфейсы.
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};