Перейти к основному содержимому

Объекты и классы в TypeScript

Разработчику Архитектору

Дальше: Коллекции · Паттерны · Синтаксис · Справочник — интерфейсы и классы

Учебное раскрытие Справочник — классы и интерфейсы (синтаксис).


В TypeScript объекты описывают через interface и type, поведение инкапсулируют в class — как в JavaScript ES6, но с проверкой контракта и модификаторами на этапе компиляции. Ключевая идея раздела — структурная типизация: важна форма, а не имя класса.

Маршрут: Типы и типизацияобъекты и классыколлекцииПаттерны.

База JS: классы и прототипы. Для сравнения номинальной модели (C#) — классы C#. Таблицы модификаторов — Справочник — классы.


Объектный контракт: interface и type

Оба описывают форму объекта. Для учебных контрактов чаще начинают с interface:

interface Person {
readonly id: string;
name: string;
age?: number;
}

const anna: Person = {
id: "p-1",
name: "Анна",
};

Разбор:

  • readonly — поле нельзя переназначить после создания (поверхностно).
  • age? — опциональное свойство (age: number | undefined).
  • Различия interface vs type (union, merge) — 8.md, не дублируем здесь.

Структурная типизация

TypeScript сравнивает типы по набору полей, не по имени объявления (в отличие от C#/Java):

interface Point {
x: number;
y: number;
}

function distance(p: Point): number {
return Math.hypot(p.x, p.y);
}

// Явный implements не нужен — форма совпала
distance({ x: 3, y: 4 });
TypeScriptC# (номинальный)
СовместимостьДостаточно нужных полейСовпадает именованный тип
Лишние полячасто ошибка (excess property check)зависит от сценария
implementsдокументирует намерениеобязателен для контракта

Разбор:

  • Структурность близка к утиной проверке JS, но ошибки ловятся в IDE.
  • implements в TS проверяет класс на соответствие контракту; для внешнего кода тип остаётся structural.

Класс с параметрами конструктора

interface Named {
name: string;
}

class User implements Named {
constructor(
public readonly id: string,
public name: string,
private role: "admin" | "user",
) {}

isAdmin(): boolean {
return this.role === "admin";
}
}

const u = new User("u-1", "Иван", "user");
console.log(u.name, u.isAdmin());

Разбор:

  • public / private в конструкторе — сокращение объявления поля (parameter properties).
  • private role доступен только внутри класса; снаружи u.role — ошибка компиляции.
  • В runtime остаётся обычный JS-класс; private — проверка TS (или # в JS — ниже).

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

МодификаторГде видноПо умолчанию
publicвездеда
privateтолько класс
protectedкласс и наследники
readonlyприсвоение один раз
class Account {
protected balance = 0;

deposit(amount: number): void {
if (amount <= 0) throw new Error("amount > 0");
this.balance += amount;
}

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

class SavingsAccount extends Account {
addInterest(rate: number): void {
this.balance *= 1 + rate;
}
}

Разбор:

  • protected даёт доступ наследнику, но не внешнему коду.
  • readonly на поле класса — как у свойства объекта в 10.md.

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

С ES2022 есть настоящие приватные поля runtime:

class Counter {
#value = 0;

increment(): void {
this.#value += 1;
}

read(): number {
return this.#value;
}
}

Разбор:

  • #value существует в скомпилированном JS и недоступен снаружи даже через (obj as any).
  • private в TS — в основном compile-time (кроме режимов с эмиссией useDefineForClassFields и т.д.).

В новых проектах для скрытия состояния часто выбирают # или замыкания, а private — для совместимости с legacy.


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

class Entity {
constructor(public id: string) {}
}

class Product extends Entity {
constructor(
id: string,
public title: string,
public price: number,
) {
super(id);
}

label(): string {
return `${this.title} (${this.price})`;
}
}

Разбор:

  • extends — как в JS; TS проверяет вызов super() до this в производном конструкторе.
  • Поля базового класса доступны в методах потомка.

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

abstract class Repository<T extends { id: string }> {
abstract findById(id: string): Promise<T | null>;

protected log(message: string): void {
console.log(`[repo] ${message}`);
}

async mustFindById(id: string): Promise<T> {
const item = await this.findById(id);
if (!item) throw new Error(`Not found: ${id}`);
return item;
}
}

class UserRepo extends Repository<{ id: string; name: string }> {
async findById(id: string) {
this.log(`find ${id}`);
return { id, name: "Demo" };
}
}

Разбор:

  • abstract метод без тела — обязан реализовать потомок.
  • Общая логика (mustFindById) живёт в базовом классе — шаблонный метод.
  • Дженерик T extends { id: string }24.md.

implements нескольких интерфейсов

interface Serializable {
toJSON(): string;
}

interface Timestamped {
updatedAt: Date;
}

class Article implements Serializable, Timestamped {
constructor(
public title: string,
public updatedAt: Date,
) {}

toJSON(): string {
return JSON.stringify({ title: this.title, updatedAt: this.updatedAt });
}
}

Разбор:

  • Класс обязан содержать все члены интерфейсов (или наследовать реализацию).
  • Интерфейсы можно расширять: interface Admin extends User { permissions: string[] }Справочник — интерфейсы.

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

class IdGenerator {
private static seq = 0;

static next(prefix: string): string {
IdGenerator.seq += 1;
return `${prefix}-${IdGenerator.seq}`;
}
}

console.log(IdGenerator.next("user"));

Разбор:

  • static принадлежит конструктору функции-класса, не экземпляру.
  • Типизация как в C#, но без namespace-классов как в старом JS.

Композиция против наследования

Не каждая сущность должна быть классом. Частый стиль в TS/React:

interface CartState {
items: { sku: string; qty: number }[];
}

function addItem(state: CartState, sku: string): CartState {
const existing = state.items.find((i) => i.sku === sku);
if (existing) {
return {
items: state.items.map((i) =>
i.sku === sku ? { ...i, qty: i.qty + 1 } : i,
),
};
}
return { items: [...state.items, { sku, qty: 1 }] };
}

Разбор:

  • Иммутабельное обновление + чистые функции хорошо стыкуются со structural typing.
  • Классы уместны, когда нужны инстансы с инвариантами (подключение к БД, жизненный цикл) — 26.md.

Класс vs type для объектов API

СценарийПредпочтение
DTO, JSON, props Reactinterface / type
Поведение + состояние, несколько методовclass
Утилиты без thisфункции + type
Плагины с фабрикойclass или объект с методами

Публичный API библиотеки часто экспортирует интерфейсы, а реализацию прячет за class или функциями.


this в методах

class Clicker {
count = 0;

handleClick = (): void => {
this.count += 1;
};
}

Разбор:

  • Стрелочное поле фиксирует this для передачи в addEventListener — см. 14.md.
  • Обычный handleClick() { } теряет this при отрыве от объекта.

Частые ошибки

ОшибкаПричинаЧто делать
Ожидали номинальность как в C#другая модельсмотреть на форму объекта, не на имя класса
private виден в runtimeтолько TS# или не экспортировать
Глубокая иерархия классовхрупкий дизайнкомпозиция, 28.md
Лишние поля в литералеexcess property checkпеременная + присвоение или индексная сигнатура
Дублирование interface и classшумодин источник правды для полей
implements без нуждыцеремониядостаточно structural match

Практика

  1. Опишите Product через interface, реализуйте Cart как объект + функции add/remove с иммутабельными обновлениями.
  2. Сделайте class InMemoryProductRepo extends Repository<Product> с findById.
  3. Добавьте implements Serializable и метод toJSON.
  4. Сравните доступ к private и # — попробуйте обратиться снаружи.
  5. Нарисуйте (на бумаге) когда вы выберете class, а когда только type для нового модуля.

Смежные статьи