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

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

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

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

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

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

Сначала — общие понятия (раздел 4 "Код")

Если ООП для вас новое, сначала пройдите материалы без привязки к синтаксису: парадигмы и уровни абстракции, затем ООП — о разделезачем объекты, введение, абстракция, инкапсуляция, наследование, полиморфизм.

Ниже — как это устроено в 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 });
TypeScriptC# (номинальный)
СовместимостьДостаточно нужных полейСовпадает именованный тип
Лишние полячасто ошибка (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Загрузка примера кода…

Разбор:


Приватные поля 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 Reactinterface / 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

Критерийinterfaceabstract 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;
}
}

Разбор:


Сравнение TypeScript с C# и Java

ТемаTypeScriptC# / Java
Модель типовСтруктурнаяНоминальная
Интерфейс + классimplements опционален для совместимостиimplements обязателен для контракта
Приватность runtime# или соглашенияprivate в IL/JVM
Множественное наследование классовнетнет
Значимые типынет (есть примитивы)struct в C#
NullT | null, ?nullable reference types / Optional

Паттерны в объектном TS

  • Фабрикаfunction createUser(...): User или статический User.create.
  • Стратегия — передача объекта с методом или функции в параметр.
  • Композицияclass Order { constructor(private readonly repo: ProductRepo) {} }.
  • Branded typestype UserId = string & { readonly brand: unique symbol } — см. Паттерны.

Чек-лист понимания

  1. Объяснить разницу структурной и номинальной типизации на примере Point.
  2. Создать класс с private и # и показать разницу при (obj as any).
  3. Написать иерархию Shape / Circle с override.
  4. Реализовать Repository<T> с абстрактным findById.
  5. Выбрать для модуля API: только interface или class — и обосновать.

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

ОшибкаПричинаЧто делать
Ожидали номинальность как в C#другая модельсмотреть на форму объекта, не на имя класса
private виден в runtimeтолько TS# или не экспортировать
Глубокая иерархия классовхрупкий дизайнкомпозиция, Паттерны в TypeScript
Лишние поля в литерале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. Выбрать для модуля API только interface или class и обосновать выбор.

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

Содержание