TypeScript. Начало работы, часть 6 из 7
08.04.2023
Теги: JavaScript • TypeScript • Web-разработка • Класс • ООП • Теория • ТипыДанных
Объектно-ориентированное программирование
1. Классы
TypeScript реализует объектно-ориентированный подход, в нем есть полноценная поддержка классов. Класс представляет шаблон для создания объектов и инкапсулирует функциональность, которую должен иметь объект. Класс определяет состояние и поведение, которыми обладает объект.
Определение класса
Для определения нового класса применяется ключевое слово class
.
class User {}
const userOne: User = new User();
const userTwo: User = new User();
Поля класса
Для хранения состояния объекта в классе определяются поля.
class User {
name: string;
age: number;
}
const user: User = new User();
user.name = 'Сергей';
user.age = 36;
console.log(`Имя ${user.name}, возраст ${user.age}`);
При определении полей им можно задать начальные значения.
class User {
name: string = 'Сергей';
age: number = 36;
}
const user: User = new User();
console.log(`Имя ${user.name}, возраст ${user.age}`);
Методы класса
Классы также могут определять поведение — некоторые действия, которые должны выполнять объекты этого класса. Для этого внутри класса определяются функции, которые называются методами.
class User {
name: string;
age: number;
print() {
console.log(`Имя ${this.name}, возраст ${this.age}`);
}
}
const user: User = new User();
user.name = 'Сергей';
user.age = 36;
user.print();
Для обращения внутри методов к полям и другим методам класса применяется ключевое слово this
, которое указывает на текущий объект этого класса.
Конструкторы
Кроме обычных методов классы имеют специальные функции — конструкторы, которые выполняют начальную инициализацию объекта.
class User {
name: string;
age: number;
constructor(userName: string, userAge: number) {
this.name = userName;
this.age = userAge;
}
print() {
console.log(`Имя ${this.name}, возраст ${this.age}`);
}
}
const user: User = new User('Сергей', 36);
user.print();
Поля для чтения
Полям класса в процессе работы можно присваивать различные значения, которые соответствуют типу полей. Однако TypeScript также позволяет определять поля только для чтения, значения которых нельзя изменить (кроме как в конструкторе).
class User {
// значение readonly поля можно установить при объявлении
readonly name: string = 'Unknown';
age: number;
constructor(userName: string, userAge: number) {
// значение readonly поля можно установить в конструкторе
this.name = userName;
this.age = userAge;
}
print() {
console.log(`Имя ${this.name}, возраст ${this.age}`);
}
}
const user: User = new User('Сергей', 36);
// значение readonly поля нельзя изменять, оно только для чтения
user.name = 'Николай'; // ошибка
user.print();
2. Наследование
Одним из ключевых моментов объектно-ориентированной парадигмы является наследование.
class Person {
name: string;
constructor(userName: string) {
this.name = userName;
}
print(): void {
console.log(`Имя: ${this.name}`);
}
}
class Employee extends Person {
company = 'unknown';
work(): void {
console.log(`${this.name} работает в компании ${this.company}`);
}
}
const employee: Employee = new Employee('Сергей');
employee.work();
Класс Employee
, который представляет работника, является подклассом или наследуется от класса Person
. А класс Person
называется родительским или базовым классом. При наследовании класс Employee
перенимает весь функционал класса Person
— все его свойства и методы. Также можно определить в подклассе новые свойства и методы.
Переопределение конструктора
Если подкласс определяет свой конструктор, то в нем должен быть вызван конструктор базового класса с помощью ключевого слова super
.
class Person {
name: string;
constructor(userName: string) {
this.name = userName;
}
print(): void {
console.log(`Имя: ${this.name}`);
}
}
class Employee extends Person {
company = 'unknown';
constructor(name: string, company: string) {
super(name);
this.company = company;
}
work(): void {
console.log(`${this.name} работает в компании ${this.company}`);
}
}
const employee: Employee = new Employee('Сергей', 'Аврора');
employee.work();
С помощью ключевого слова super
подкласс может обратиться к функционалу базового класса. В данном случае идет обращение к конструктору класса Person
, который устанавливает значение свойства name
.
Переопределение методов
Также производные классы могут переопределять методы базовых классов.
class Person {
name: string;
constructor(userName: string) {
this.name = userName;
}
print(): void {
console.log(`Имя ${this.name}`);
}
}
class Employee extends Person {
company = 'unknown';
constructor(name: string, company: string) {
super(name);
this.company = company;
}
print(): void {
super.print();
console.log(`Работает в компании ${this.company}`);
}
}
const employee: Employee = new Employee('Сергей', 'Аврора');
employee.print();
3. Абстрактные классы
Абстрактные классы во многом похожи на обычные классы за тем исключением, что нельзя создать напрямую объект абстрактного класса, используя его конструктор. Как правило, абстрактные классы описывают сущности, которые в реальности не имеют конкретного воплощения.
Например, геометрическая фигура может представлять круг, квадрат, треугольник, но как таковой геометрической фигуры самой по себе не существует. В то же время все фигуры могут иметь какой-то общий функционал. В этом случае можно определить абстрактный класс фигуры, поместить в него общий функционал, и от него унаследовать классы круга, квадрата, треугольника.
abstract class Figure {
getArea(): void {
console.log('Not implemented');
}
}
class Rectangle extends Figure {
constructor(public width: number, public height: number) {
super();
}
getArea(): void {
const square = this.width * this.height;
console.log('area =', square);
}
}
const rectangle: Figure = new Rectangle(20, 30);
rectangle.getArea(); // area = 600
Абстрактные методы
Однако в данном случае метод getArea
в базовом классе не выполняет никакой полезной работы, так как у абстрактной фигуры не может быть площади. И в этом случае подобный метод лучше определить как абстрактный.
abstract class Figure {
abstract getArea(): void;
}
class Rectangle extends Figure {
constructor(public width: number, public height: number) {
super();
}
getArea(): void {
const square = this.width * this.height;
console.log('area =', square);
}
}
const rectangle: Figure = new Rectangle(20, 30);
rectangle.getArea(); // area = 600
Абстрактный метод не может иметь реализации. Если класс содержит абстрактные методы, то такой класс должен быть абстрактным. Кроме того, при наследовании производные классы обязаны реализовать все абстрактные методы.
Абстрактные поля
Также абстрактный класс может иметь абстрактные поля, класс-наследник обязан предоставить для них реализацию.
abstract class Figure {
abstract x: number;
abstract y: number;
abstract getArea(): void;
}
class Rectangle extends Figure {
x: number;
y: number;
width: number;
height: number;
constructor(x: number, y: number, width: number, height: number) {
super();
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
getArea(): void {
const square = this.width * this.height;
console.log('area =', square);
}
}
const rectangle: Figure = new Rectangle(10, 10, 20, 25);
rectangle.getArea();
abstract class Figure {
abstract x: number;
abstract y: number;
abstract getArea(): void;
}
class Rectangle extends Figure {
// сокращенный вариант объявления и инициализации полей класса
constructor(
public x: number,
public y: number,
public width: number,
public height: number
) {
super();
}
getArea(): void {
const square = this.width * this.height;
console.log('area =', square);
}
}
const rectangle: Figure = new Rectangle(10, 10, 20, 25);
rectangle.getArea();
4. Модификаторы доступа
Модификаторы доступа public
, protected
и private
позволяют разрешать или запрещать доступ извне к свойствам и методам класса. Если модификатор не используется, то такое свойство или метод расцениваются как public
.
class Person {
name: string;
year: number;
}
class Person {
public name: string;
public year: number;
}
Модификатор private
Если к свойствам и методам класса применяется модификатор private
, то к ним нельзя будет обратиться из внешнего кода, который использует этот класс. В том числе, нельзя будет обратиться даже из класса-наследника.
class Person {
private _name: string;
private _year: number;
constructor(name: string, age: number) {
this._name = name;
this._year = this.setYear(age);
}
public print(): void {
console.log(`Имя ${this._name}, год рождения ${this._year}`);
}
private setYear(age: number): number {
return new Date().getFullYear() - age;
}
}
const person: Person = new Person('Сергей', 36);
person.print();
console.log(person._name); // ошибка
person.setYear(45); // ошибка
Модификатор protected
Если к свойствам и методам класса применяется модификатор protected
, то к ним можно будет обратиться только из класса-наследника.
class Person {
protected name: string;
private year: number;
constructor(name: string, age: number) {
this.name = name;
this.year = this.setYear(age);
}
protected print(): void {
console.log(`Имя ${this.name}, год рождения ${this.year}`);
}
private setYear(age: number): number {
return new Date().getFullYear() - age;
}
}
class Employee extends Person {
protected company: string;
constructor(name: string, age: number, company: string) {
super(name, age);
this.company = company;
}
public print(): void {
console.log(`Год рождения ${this.year}`); // ошибка
super.print();
console.log(`Компания ${this.company}`);
}
}
const employee: Employee = new Employee('Сергей', 31, 'Аврора');
employee.print();
Определение полей через конструктор
Использование модификаторов доступа в параметрах конструктора позволяет сократить написание кода.
class Person {
private name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
print(): void {
console.log(`Имя ${this.name}, возраст: ${this.age}`);
}
}
class Person {
constructor(private name: string, private age: number) {}
print(): void {
console.log(`Имя ${this.name}, возраст: ${this.age}`);
}
}
5. Методы доступа get и set
TypeScript поддерживает концепцию методов доступа — для доступа к свойству определяется пара методов. Метод get
для получения значения свойства и метод set
для установки значения. Использовать геттеры и сеттеры для доступа к свойствам объекта гораздо лучше, чем обращение к свойствам напрямую. Можно реализовать валидацию на уровне реализации set
. Или, добавить логирование и обработку ошибок на уровне геттеров и сеттеров.
class Person {
public name: string;
private _age: number;
public get age(): number {
return this._age;
}
public set age(value: number) {
if (value < 0 || value > 100) {
console.log('Недопустимый возраст!');
} else {
this._age = value;
}
}
}
const person: Person = new Person();
person.name = 'Сергей';
person.age = 36;
console.log(person.age); // 36
person.age = 200; // Недопустимый возраст!
console.log(person.age); // 36
6. Статические поля и методы
Кроме обычных полей и методов класс может иметь статические. Статические поля и методы относятся не к отдельным объектам, а в целом к классу. И для обращения к статическим полям и методам применяется имя класса.
class Person {
constructor(public name: string, public age: number) {}
static retirementAge = 65;
static calculateYears(age: number): number {
return Person.retirementAge - age;
}
}
const person: Person = new Person('Сергей', 36);
const years = Person.calculateYears(36);
console.log(`Возраст выхода на пенсию ${Person.retirementAge} лет`);
console.log(`${person.name}, до пенсии осталось ${years} лет`);
class Person {
constructor(public name: string, public age: number) {}
static retirementAge = 65;
static calculateYears(age: number): number {
// так тоже можно, потому что this === Person
return this.retirementAge - age;
}
}
const person: Person = new Person('Сергей', 36);
const years = Person.calculateYears(36);
console.log(`Возраст выхода на пенсию ${Person.retirementAge} лет`);
console.log(`${person.name}, до пенсии осталось ${years} лет`);
В статических методах можно обращаться к статическим полям и методам класса, но нельзя обращаться к нестатическим полям и методам.
class Person {
constructor(public name: string, public age: number) {}
static retirementAge = 65;
static calculateYears(): number {
return Person.retirementAge - this.age; // ошибка
}
}
const person: Person = new Person('Сергей', 36);
const years = Person.calculateYears();
console.log(`Возраст выхода на пенсию ${Person.retirementAge} лет`);
console.log(`${person.name}, до пенсии осталось ${years} лет`);
Статические поля и методы могут наследоваться, что позволяет обращаться к ним через имя производного класса.
class Person {
constructor(public name: string, public age: number) {}
static retirementAge = 65;
static calculateYears(age: number): number {
return this.retirementAge - age;
}
}
class Employee extends Person {}
const employee: Employee = new Employee('Сергей', 36);
const years = Employee.calculateYears(36); // метод унаследован
console.log(`Возраст выхода на пенсию ${Employee.retirementAge} лет`);
console.log(`${employee.name}, до пенсии осталось ${years} лет`);
Как и обычные поля и методы, статические могут иметь модификаторы доступа public
, protected
и private
.
7. Интерфейсы классов
Интерфейсы могут быть реализованы не только объектами, но и классами — для этого используется ключевое слово implements
.
interface IUser {
name: string;
surname: string;
getFullName(): string;
}
class User implements IUser {
name: string;
surname: string;
age: number;
constructor(userName: string, userSurname: string, userAge: number) {
this.name = userName;
this.surname = userSurname;
this.age = userAge;
}
getFullName(): string {
return this.name + ' ' + this.surname;
}
}
const user: IUser = new User('Сергей', 'Иванов', 36);
console.log(user.getFullName());
8. Приведение к типу
Мы уже говорили о приведении к типу — от более общего, например number|string
к более конкретному, например к number
или string
. Аналогично можно привести к более конкретному типу переменную для хранения экземпляра класса.
class Person {
name: string;
constructor(userName: string) {
this.name = userName;
}
}
class Employee extends Person {
company: string;
constructor(userName: string, userCompany: string) {
super(userName);
this.company = userCompany;
}
}
Здесь класс Employee
унаследован от класса Person
. Поскольку объекты Employee
в то же время являются и объектами Person
, то при определении объектов можно написать вот так.
const person: Person = new Employee('Сергей', 'Автора');
Здесь продемонстрировано восходящее преобразование, то есть от более конкретного типа к более общему — от производного типа Employee
к базовому типу Person
. Оно производится неявно — не нужно писать какой-то дополнительный код. Но есть и другой тип преобразования — нисходящий или от более общего типа к более конкретному.
class Person {
name: string;
constructor(userName: string) {
this.name = userName;
}
}
class Employee extends Person {
company: string;
constructor(userName: string, userCompany: string) {
super(userName);
this.company = userCompany;
}
}
const person: Person = new Employee('Сергей', 'Аврора');
console.log(person.company); // ошибка
Здесь переменная person
имеет тип Person
, однако в реальности эта переменная указывает на объект типа Employee
. Попытка вывести значение свойства company
у объекта person
завершается ошибкой — такого свойства нет у типа Person
. Чтобы решить эту проблему — нужно явно привести объект person
к типу Employee
.
const person: Person = new Employee('Сергей', 'Аврора');
const employee: Employee = person as Employee; // приведение к типу Employee
console.log(employee.company);
const person: Person = new Employee('Сергей', 'Аврора');
const employee: Employee = <Employee>person; // приведение к типу Employee
console.log(employee.company);
Поиск: JavaScript • TypeScript • Web-разработка • Теория • Типы данных