Объекты и классы в TypeScript
Дальше: Коллекции · Паттерны · Синтаксис · Справочник — интерфейсы и классы
В TypeScript объекты описывают через interface и type, поведение инкапсулируют в class — как в JavaScript ES6, но с проверкой контракта и модификаторами на этапе компиляции. Ключевая идея раздела — структурная типизация (structural typing): совместимость определяется набором полей и методов, без привязки к имени типа. Удобный порядок — типы и типизация, эта статья, коллекции, паттерны. Таблицы — Справочник — классы и интерфейсы (синтаксис).
База JS: классы и прототипы. Для сравнения номинальной модели (C#) — классы C#. Таблицы модификаторов — Справочник — классы.
Если ООП для вас новое, сначала пройдите материалы без привязки к синтаксису: парадигмы и уровни абстракции, затем ООП — о разделе — зачем объекты, введение, абстракция, инкапсуляция, наследование, полиморфизм.
Ниже — как это устроено в TypeScript поверх JavaScript-классов ES6.
Теория и синтаксис TypeScript
| Понятие ООП | Как выражено в TypeScript |
|---|---|
| Контракт данных | interface, type — структурная совместимость (форма объекта) |
| Класс, АДТ | class; в runtime — прототип JS |
| Создание экземпляра | new ClassName(), фабрики, литералы объектов |
| Конструктор | constructor(...); parameter properties |
| Инкапсуляция | private / protected / public; # в runtime |
| Наследование | extends; один базовый класс |
| Полиморфизм подтипов | переопределение методов; implements интерфейсов |
| Абстракция | abstract class, интерфейсы без реализации |
| Статика | static поля и методы на функции-конструкторе |
| Параметрический полиморфизм | дженерики class Repo<T> |
Определения без привязки к языку — раздел 4-08-oop.
Кратко для новичка:
- Класс — шаблон с полями и методами;
newсоздаёт экземпляр в памяти. interface/type— описание формы данных без реализации; TypeScript проверяет совпадение полей (структурная типизация).- Инкапсуляция — скрытие полей через
privateили#; снаружи работают только методы. - Наследование —
extendsпереносит поля и методы родителя;overrideпомечает переопределение. - Полиморфизм — переменная типа
Shapeможет ссылаться наCircle; вызов метода идёт по реальному классу объекта.
Четыре столпа ООП в TypeScript
Интерактивные схемы — общие принципы (псевдокод). Подробнее: инкапсуляция, наследование, полиморфизм, абстракция.
Play ITЗагрузка интерактивного демо…
Play ITЗагрузка интерактивного демо…
Play ITЗагрузка интерактивного демо…
Play ITЗагрузка интерактивного демо…
В TypeScript все четыре идеи работают как в JS, но ошибки нарушения контракта ловятся до запуска (в IDE и при tsc). Ключевое отличие от C#/Java — типы структурные: совместимость по форме полей и методов, без привязки к имени класса (см. полиморфизм).
Создание экземпляра и запись в переменные
Литерал объекта (без class)
const point: { x: number; y: number } = { x: 10, y: 20 };
let p = point; // копируется ссылка на тот же объект
p.x = 99; // меняется и point.x
Разбор:
const point— переменная не переназначается на другой объект; поля внутри менять можно.- Присваивание
let p = pointкопирует ссылку, без клонирования объекта (как в JS). - Для DTO и API чаще используют именованный
interfaceвместо inline-типа.
Через new и конструктор класса
class User {
constructor(
public readonly id: string,
public name: string,
) {}
}
const u1 = new User("u-1", "Анна");
const u2: User = u1; // тот же экземпляр по ссылке
const u3 = new User("u-2", "Борис"); // другой объект
Разбор:
new User(...)вызываетconstructor, создаёт объект в куче, возвращает ссылку.- Аннотация
: Userна переменной — проверка типа; в runtime это обычный объект. readonly idв parameter property — поле задаётся один раз в конструкторе.
Фабричные функции
interface Product {
sku: string;
price: number;
}
function createProduct(sku: string, price: number): Product {
return { sku, price };
}
const item = createProduct("A-1", 100);
Разбор:
- Фабрика возвращает объект, совместимый с
Productструктурно —implementsне нужен. - Популярный стиль в React/Node: поведение — функции, данные — plain objects +
type.
Порядок инициализации полей класса
class Demo {
a = this.b + 1; // поля инициализируются сверху вниз
b = 10;
}
Разбор:
- Поля класса инициализируются до тела конструктора (с учётом
useDefineForClassFieldsвtsconfig). - В производном классе сначала инициализируются поля базового, затем производного, затем
constructorпроизводного (послеsuper()).
Конструкторы, все формы
Обычный конструктор
class BankAccount {
private balance: number;
constructor(initial: number) {
if (initial < 0) throw new Error("Отрицательный баланс");
this.balance = initial;
}
deposit(amount: number): void {
if (amount > 0) this.balance += amount;
}
getBalance(): number {
return this.balance;
}
}
Разбор:
- Инвариант (правило состояния, например "баланс ≥ 0") задают в конструкторе и методах — это инкапсуляция поведения (инкапсуляция).
private balanceнедоступен снаружи; клиент вызывает толькоdeposit/getBalance.
Parameter properties (сокращение полей)
class Employee {
constructor(
public name: string,
private salary: number,
protected department: string,
) {}
}
Разбор:
- Модификатор в параметре конструктора одновременно объявляет поле класса и присваивает аргумент.
- Эквивалентно ручному
this.name = nameв теле конструктора. protected departmentвиден в подклассах, но не снаружи.
Перегрузка конструктора (сигнатуры)
class Point {
constructor(x: number, y: number);
constructor(coords: { x: number; y: number });
constructor(xOrCoords: number | { x: number; y: number }, y?: number) {
if (typeof xOrCoords === "number") {
this.x = xOrCoords;
this.y = y!;
} else {
this.x = xOrCoords.x;
this.y = xOrCoords.y;
}
}
x: number;
y: number;
}
Разбор:
- Несколько сигнатур
constructor(...)— только для типизации; реализация одна. - В runtime остаётся один конструктор JS.
Правило super() в наследнике
class Animal {
constructor(public name: string) {}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name); // обязательно до использования this
// this.breed = breed — уже сделано parameter property
}
}
Разбор:
- В производном классе до обращения к
thisнужно вызватьsuper(...). - TypeScript проверяет это статически; иначе runtime-ошибка в JS.
Объектный контракт через 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иtype(union, merge) — в Синтаксис и пунктуация TypeScript.
Структурная типизация
Структурная типизация (structural typing) — 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 | документирует намерение | обязателен для контракта |
Разбор:
- Структурность близка к утиной типизации (duck typing) в JS, но ошибки ловятся в IDE.
implementsв TS проверяет класс на соответствие контракту; для внешнего кода тип остаётся structural.
Класс с параметрами конструктора
Код ITЗагрузка примера кода…
Разбор:
public/privateв конструкторе — сокращение объявления поля (parameter properties).private roleдоступен только внутри класса; снаружиu.role— ошибка компиляции.- В runtime остаётся обычный JS-класс;
private— проверка TS (или#в JS — ниже).
Модификаторы доступа
| Модификатор | Где видно | По умолчанию |
|---|---|---|
public | везде | да |
private | только класс | — |
protected | класс и наследники | — |
readonly | присвоение один раз | — |
Код ITЗагрузка примера кода…
Разбор:
protectedдаёт доступ наследнику, но не внешнему коду.readonlyна поле класса — как у свойства объекта в Типы данных и типизация в TypeScript.
Приватные поля 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
Код ITЗагрузка примера кода…
Разбор:
extends— как в JS; TS проверяет вызовsuper()доthisв производном конструкторе.- Поля базового класса доступны в методах потомка.
Абстрактные классы
Код ITЗагрузка примера кода…
Разбор:
abstractметод без тела — обязан реализовать потомок.- Общая логика (
mustFindById) живёт в базовом классе — шаблонный метод. - Дженерик
T extends { id: string }— Дженерики в TypeScript.
implements нескольких интерфейсов
Код ITЗагрузка примера кода…
Разбор:
- Класс обязан содержать все члены интерфейсов (или наследовать реализацию).
- Интерфейсы можно расширять:
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:
Код ITЗагрузка примера кода…
Разбор:
- Иммутабельное обновление и чистые функции хорошо стыкуются со structural typing.
- Классы уместны, когда нужны инстансы с инвариантами (подключение к БД, жизненный цикл) — TypeORM, наследование.
Класс и 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— см. Функции в TypeScript. - Обычный
handleClick() { }теряетthisпри отрыве от объекта.
Геттеры, сеттеры и вычисляемые свойства
class Rectangle {
constructor(
private _width: number,
private _height: number,
) {}
get width(): number {
return this._width;
}
set width(value: number) {
if (value <= 0) throw new Error("Ширина должна быть > 0");
this._width = value;
}
get area(): number {
return this._width * this._height;
}
}
Разбор:
get/set— синтаксический сахар: снаружиrect.width = 5вызывает сеттер.- Вычисляемое свойство
areaбезset— только для чтения. - Подчёркивание в
_width— соглашение о внутреннем поле; TS не запрещает доступ снаружи, если неprivate.
Инкапсуляция через private, protected, #
Уровни доступа
| Модификатор | Класс | Наследник | Внешний код | Runtime |
|---|---|---|---|---|
public | да | да | да | да |
protected | да | да | нет | да (в JS поле есть) |
private | да | нет* | нет | compile-time** |
#field | да | нет | нет | настоящая приватность |
* В TS 4.3+ private в базовом классе не виден в подклассе (номинальная приватность на уровне типов).
** После компиляции в JS поле private может существовать как обычное свойство.
class Base {
protected secret = "base";
private token = "hidden";
}
class Child extends Base {
reveal() {
return this.secret; // OK
// return this.token; // ошибка TS
}
}
Разбор:
protected— типичный выбор для расширяемого API базового класса.- Для секретов (ключи, токены) предпочтительнее
#или замыкания в фабрике.
Наследование и переопределение
extends, super, override
class Shape {
constructor(protected color: string) {}
describe(): string {
return `фигура цвета ${this.color}`;
}
}
class Circle extends Shape {
constructor(color: string, public radius: number) {
super(color);
}
override describe(): string {
return `${super.describe()}, r=${this.radius}`;
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
Разбор:
override(TS 4.3+) — явная пометка переопределения; ошибка, если в базе нет такого метода.super.describe()— вызов реализации родителя.- Полиморфная переменная:
const s: Shape = new Circle("red", 5); s.describe()— вызоветсяCircle.describe.
Один базовый класс, много интерфейсов
interface Serializable {
toJSON(): unknown;
}
interface Identifiable {
readonly id: string;
}
class Entity implements Identifiable, Serializable {
constructor(public readonly id: string, public name: string) {}
toJSON() {
return { id: this.id, name: this.name };
}
}
Разбор:
- Класс наследует один класс, но может
implementsнесколько интерфейсов. - Интерфейсы в TS не существуют в runtime — только проверка компилятора.
Полиморфизм
Структурный полиморфизм (утиная типизация и типы)
interface Drawable {
draw(ctx: CanvasRenderingContext2D): void;
}
function renderAll(items: Drawable[], ctx: CanvasRenderingContext2D) {
for (const item of items) {
item.draw(ctx);
}
}
// Класс без implements — всё равно подходит, если есть draw
class Label {
constructor(private text: string) {}
draw(ctx: CanvasRenderingContext2D) {
ctx.fillText(this.text, 0, 0);
}
}
Разбор:
renderAllпринимает всё, у чего естьdrawс нужной сигнатурой.implements Drawableдокументирует намерение; для вызоваrenderAllдостаточно совпадения формы методаdraw.
Полиморфизм через union и narrowing
type Payment = CardPayment | CashPayment;
interface CardPayment {
kind: "card";
last4: string;
}
interface CashPayment {
kind: "cash";
amount: number;
}
function total(p: Payment): number {
switch (p.kind) {
case "card":
return 0; // комиссия отдельно
case "cash":
return p.amount;
}
}
Разбор:
- Дискриминант
kind— идиома TS вместо глубокой иерархии классов. - Компилятор сужает тип в каждой ветке
switch.
instanceof (номинальный след в runtime)
if (error instanceof TypeError) {
console.error(error.message);
}
Разбор:
instanceofпроверяет цепочку прототипов — работает для классов; дляinterfaceв runtime проверки нет.- Для plain objects используют type guards:
"kind" in obj.
Абстракция через интерфейсы и абстрактные классы
Таблица, термины is-a / can-do и пример умного дома — Абстракция в ООП.
Когда interface, когда abstract class
| Критерий | interface | abstract class |
|---|---|---|
| Роль | Структурный контракт — "есть такие методы" | Номинальная иерархия — явный родитель |
| После компиляции | Тип исчезает, остаётся JavaScript | Класс есть в runtime |
| Поля экземпляра | Только сигнатуры свойств | Можно protected items и общую логику |
| Готовый код | Только сигнатуры методов | Да, например mustFindById ниже |
| Сколько можно указать | extends у нескольких интерфейсов | Один extends у класса |
В TypeScript типизация структурная — объект может подойти под interface, даже если класс формально его не implements. abstract class задаёт и контракт, и общую реализацию для наследников.
abstract class Repository<T extends { id: string }> {
protected items = new Map<string, T>();
abstract findById(id: string): T | undefined;
mustFindById(id: string): T {
const item = this.findById(id);
if (!item) throw new Error(`Not found: ${id}`);
return item;
}
}
Разбор:
mustFindById— шаблонный метод, общая логика в базе (абстракция).findById— контракт, который допишет подкласс (InMemoryProductRepo).
Сравнение TypeScript с C# и Java
| Тема | TypeScript | C# / Java |
|---|---|---|
| Модель типов | Структурная | Номинальная |
| Интерфейс + класс | implements опционален для совместимости | implements обязателен для контракта |
| Приватность runtime | # или соглашения | private в IL/JVM |
| Множественное наследование классов | нет | нет |
| Значимые типы | нет (есть примитивы) | struct в C# |
| Null | T | null, ? | nullable reference types / Optional |
Паттерны в объектном TS
- Фабрика —
function createUser(...): Userили статическийUser.create. - Стратегия — передача объекта с методом или функции в параметр.
- Композиция —
class Order { constructor(private readonly repo: ProductRepo) {} }. - Branded types —
type UserId = string & { readonly brand: unique symbol }— см. Паттерны.
Чек-лист понимания
- Объяснить разницу структурной и номинальной типизации на примере
Point. - Создать класс с
privateи#и показать разницу при(obj as any). - Написать иерархию
Shape/Circleсoverride. - Реализовать
Repository<T>с абстрактнымfindById. - Выбрать для модуля API: только
interfaceилиclass— и обосновать.
Частые ошибки
| Ошибка | Причина | Что делать |
|---|---|---|
| Ожидали номинальность как в C# | другая модель | смотреть на форму объекта, не на имя класса |
private виден в runtime | только TS | # или не экспортировать |
| Глубокая иерархия классов | хрупкий дизайн | композиция, Паттерны в TypeScript |
| Лишние поля в литерале | excess property check | переменная + присвоение или индексная сигнатура |
Дублирование interface и class | шум | один источник правды для полей |
implements без нужды | церемония | достаточно structural match |
Практика
- Опишите
Productчерезinterface, реализуйтеCartкак объект + функцииadd/removeс иммутабельными обновлениями. - Сделайте
class InMemoryProductRepo extends Repository<Product>сfindById. - Добавьте
implements Serializableи методtoJSON. - Сравните доступ к
privateи#— попробуйте обратиться снаружи. - Выбрать для модуля API только
interfaceилиclassи обосновать выбор.
Смежные статьи
- Типы — structural typing,
readonly - Синтаксис —
interfaceиtype,import type - Коллекции — массивы в полях класса
- Дженерики —
Repository<T> - Паттерны — branded types, фабрики
- Справочник — Справочник — интерфейсы и классы
- JS runtime: Работа с объектами и прототипами · C#: Объектно-ориентированное программирование в C# · Введение в ООП