Объекты и классы в 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).- Различия
interfacevstype(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 });
| TypeScript | C# (номинальный) | |
|---|---|---|
| Совместимость | Достаточно нужных полей | Совпадает именованный тип |
| Лишние поля | часто ошибка (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 React | interface / 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 |
Практика
- Опишите
Productчерезinterface, реализуйтеCartкак объект + функцииadd/removeс иммутабельными обновлениями. - Сделайте
class InMemoryProductRepo extends Repository<Product>сfindById. - Добавьте
implements Serializableи методtoJSON. - Сравните доступ к
privateи#— попробуйте обратиться снаружи. - Нарисуйте (на бумаге) когда вы выберете
class, а когда толькоtypeдля нового модуля.
Смежные статьи
- Типы — structural typing,
readonly - Синтаксис —
interfacevstype,import type - Коллекции — массивы в полях класса
- Дженерики —
Repository<T> - Паттерны — branded types, фабрики
- Справочник — Справочник — интерфейсы и классы
- JS runtime: 22 · C#: 25