Объявление класса
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
}
}
Практика
- Создайте класс
Counterс приватным полемcount, методамиinc(),dec()и геттеромvalue. - Реализуйте иерархию
Animal→Dog,Catс методомsound(). - Опишите интерфейс
Comparableс методомcompareTo(other): numberи реализуйте его в классеMoney. - Создайте абстрактный класс
PaymentMethodс подклассамиCardиCash. - Реализуйте простой репозиторий для сущности
Productпо образцу выше.
Итог
Мы изучили классы в TypeScript: поля и методы, модификаторы доступа,
readonly, сокращённую запись в конструкторе, наследование,
реализацию интерфейсов, абстрактные классы, статические члены и
геттеры/сеттеры. В следующей главе разберём дженерики — инструмент для
переиспользуемого типобезопасного кода.