Глава 5. Классы и модификаторы доступа

169 просмотров
0 лайков
0 в избранном

Объявление класса

class User {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    greet(): string {
        return `Привет, я ${this.name}`;
    }
}

const u = new User("Анна", 28);
console.log(u.greet()); // Привет, я Анна

Сокращённая запись в конструкторе

Параметр с модификатором public/private/protected автоматически становится полем класса — это экономит шаблонный код:

class User {
    constructor(
        public name: string,
        public age: number,
    ) {}
}

const u = new User("Анна", 28);
console.log(u.name); // Анна

Эквивалент полной записи:

class User {
    public name: string;
    public age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

Модификаторы доступа

  • public — доступен всем (по умолчанию).
  • private — только внутри этого класса.
  • protected — внутри класса и в подклассах.
  • readonly — нельзя переназначать после создания.
class Account {
    private balance: number;
    readonly id: number;
    protected currency: string;

    constructor(id: number, initial: number, currency: string = "RUB") {
        this.id = id;
        this.balance = initial;
        this.currency = currency;
    }

    deposit(amount: number): void {
        if (amount <= 0) throw new Error("Сумма должна быть положительной");
        this.balance += amount;
    }

    withdraw(amount: number): void {
        if (amount > this.balance) throw new Error("Недостаточно средств");
        this.balance -= amount;
    }

    getBalance(): number {
        return this.balance;
    }
}

const acc = new Account(1, 1000);
acc.deposit(500);
console.log(acc.getBalance()); // 1500
// acc.balance; // Ошибка: balance приватный

Приватные поля: TypeScript vs JavaScript

TypeScript предлагает ключевое слово private, но оно работает только на этапе компиляции. Современный JavaScript имеет настоящий private-синтаксис с префиксом #:

class Account {
    #balance: number; // настоящий private, защищён в рантайме

    constructor(initial: number) {
        this.#balance = initial;
    }
}

readonly-поля

Инициализируются в конструкторе, дальше не меняются. Удобно для идентификаторов и конфигурации:

class Order {
    constructor(
        readonly id: number,
        readonly items: string[],
        public status: string,
    ) {}
}

const order = new Order(1, ["A", "B"], "new");
// order.id = 2;      // Ошибка: readonly
order.status = "paid"; // OK

Наследование

class User {
    constructor(
        public name: string,
        public email: string,
    ) {}

    describe(): string {
        return `${this.name} (${this.email})`;
    }
}

class Admin extends User {
    constructor(
        name: string,
        email: string,
        public permissions: string[],
    ) {
        super(name, email); // вызов конструктора родителя
    }

    can(action: string): boolean {
        return this.permissions.includes(action);
    }
}

const admin = new Admin("Анна", "anna@example.com", ["read", "write"]);
console.log(admin.describe()); // Анна (anna@example.com)
console.log(admin.can("write")); // true

Переопределение методов

class Animal {
    constructor(public name: string) {}
    sound(): string {
        return "...";
    }
}

class Dog extends Animal {
    sound(): string {
        return "Гав!";
    }
}

class Cat extends Animal {
    sound(): string {
        return `${super.sound()} Мяу!`; // можно вызвать родительский через super
    }
}

Реализация интерфейса

Класс может «обязаться» реализовать интерфейс. Это удобно, когда несколько классов должны поддерживать общий контракт:

interface Serializable {
    serialize(): string;
}

class User implements Serializable {
    constructor(public name: string, public age: number) {}

    serialize(): string {
        return JSON.stringify({ name: this.name, age: this.age });
    }
}

class Product implements Serializable {
    constructor(public title: string, public price: number) {}

    serialize(): string {
        return JSON.stringify({ title: this.title, price: this.price });
    }
}

// Полиморфизм через интерфейс
function save(items: Serializable[]): string[] {
    return items.map((i) => i.serialize());
}

Абстрактные классы

Абстрактный класс нельзя инстанцировать напрямую — только наследовать. Он может содержать абстрактные методы, которые обязан реализовать подкласс:

abstract class Shape {
    abstract area(): number; // без реализации

    describe(): string {
        return `Фигура площадью ${this.area()}`;
    }
}

class Circle extends Shape {
    constructor(public radius: number) {
        super();
    }

    area(): number {
        return Math.PI * this.radius ** 2;
    }
}

class Square extends Shape {
    constructor(public side: number) {
        super();
    }

    area(): number {
        return this.side ** 2;
    }
}

// new Shape(); // Ошибка: абстрактный класс
const shapes: Shape[] = [new Circle(5), new Square(4)];
shapes.forEach((s) => console.log(s.describe()));

Статические члены

Принадлежат классу, а не экземпляру. Удобно для утилит и счётчиков:

class User {
    static count = 0; // общий счётчик для всех пользователей

    constructor(public name: string) {
        User.count++;
    }

    static create(name: string): User {
        return new User(name);
    }
}

const a = new User("Анна");
const b = User.create("Борис");
console.log(User.count); // 2

Геттеры и сеттеры

Позволяют добавить логику при чтении/записи свойства:

class Password {
    private _value: string = "";

    get value(): string {
        return this._value;
    }

    set value(newVal: string) {
        if (newVal.length < 8) {
            throw new Error("Пароль слишком короткий");
        }
        this._value = newVal;
    }
}

const pwd = new Password();
pwd.value = "verysecret";  // OK
// pwd.value = "123";       // Ошибка: слишком короткий

Кейс из реального проекта: репозиторий

Паттерн «репозиторий» — типичный пример классов в backend-приложениях. Абстрактный базовый класс задаёт контракт, конкретные репозитории реализуют его для разных сущностей:

interface Identifiable {
    id: number;
}

abstract class Repository<T extends Identifiable> {
    constructor(protected items: T[] = []) {}

    abstract create(data: Omit<T, "id">): T;

    findAll(): T[] {
        return this.items;
    }

    findById(id: number): T | undefined {
        return this.items.find((i) => i.id === id);
    }

    remove(id: number): void {
        this.items = this.items.filter((i) => i.id !== id);
    }
}

interface User extends Identifiable {
    name: string;
    email: string;
}

class UserRepository extends Repository<User> {
    private nextId = 1;

    create(data: Omit<User, "id">): User {
        const user = { ...data, id: this.nextId++ };
        this.items.push(user);
        return user;
    }

    findByEmail(email: string): User | undefined {
        return this.items.find((u) => u.email === email);
    }
}

const users = new UserRepository<User>();
const anna = users.create({ name: "Анна", email: "a@x.ru" });
console.log(users.findById(anna.id));

Типичные ошибки

1. Забыли super() в подклассе

class Admin extends User {
    constructor(name: string) {
        // super(name); // забыли — будет ошибка компиляции
        this.name = name;
    }
}

2. Доступ к приватному полю снаружи

const acc = new Account(1, 1000);
console.log(acc.balance); // Ошибка: balance приватный

3. Изменение readonly в методе

class Order {
    constructor(readonly id: number) {}

    changeId(newId: number): void {
        // this.id = newId; // Ошибка: readonly
    }
}

Практика

  1. Создайте класс Counter с приватным полем count, методами inc(), dec() и геттером value.
  2. Реализуйте иерархию AnimalDog, Cat с методом sound().
  3. Опишите интерфейс Comparable с методом compareTo(other): number и реализуйте его в классе Money.
  4. Создайте абстрактный класс PaymentMethod с подклассами Card и Cash.
  5. Реализуйте простой репозиторий для сущности Product по образцу выше.

Итог

Мы изучили классы в TypeScript: поля и методы, модификаторы доступа, readonly, сокращённую запись в конструкторе, наследование, реализацию интерфейсов, абстрактные классы, статические члены и геттеры/сеттеры. В следующей главе разберём дженерики — инструмент для переиспользуемого типобезопасного кода.

Комментарии 0

Для добавления комментариев необходимо войти или зарегистрироваться.

Пока нет комментариев. Станьте первым!