Классы

Классы в TypeScript расширяют ES6 классы типизацией свойств, implements для интерфейсов, модификаторами доступа и static-членами.

Зачем нужно

  • TypeScript добавляет к классам типизацию: свойства с типами, implements, модификаторы доступа
  • Классы — основной механизм ООП в TypeScript
  • Используются в Angular, NestJS, TypeORM и других фреймворках
  • Позволяют инкапсулировать данные и поведение с проверкой типов

Где используется

  • Backend: NestJS контроллеры, сервисы, модули
  • ORM: TypeORM, Prisma entities
  • Frontend: Angular компоненты и сервисы
  • Паттерны: Repository, Service, Factory, Singleton

Предпосылки

Базовый синтаксис

class User {
  // Объявление свойств с типами (обязательно в TS!)
  id: number;
  name: string;
  email: string;

  constructor(id: number, name: string, email: string) {
    this.id = id;
    this.name = name;
    this.email = email;
  }

  greet: string {
    return `Hello, I'm ${this.name}`;
  }
}

const user = new User(1, "Alice", "alice@example.com");
console.log(user.greet); // "Hello, I'm Alice"

Parameter Properties (сокращённый синтаксис)

TypeScript позволяет объявлять и инициализировать свойства прямо в параметрах конструктора:

class User {
  // Модификатор в параметре = объявление + инициализация
  constructor(
    public id: number,
    public name: string,
    public email: string,
    private password: string
  ) {}
  // Эквивалентно ручному: this.id = id; this.name = name; ...

  greet: string {
    return `Hello, I'm ${this.name}`;
  }
}

const user = new User(1, "Alice", "a@b.com", "secret");
user.name;     // OK — public
user.password; // Ошибка! — private

Implements — реализация интерфейсов

interface Serializable {
  serialize: string;
}

interface Validatable {
  validate: boolean;
}

class User implements Serializable, Validatable {
  constructor(
    public name: string,
    public email: string
  ) {}

  serialize: string {
    return JSON.stringify({ name: this.name, email: this.email });
  }

  validate: boolean {
    return this.name.length > 0 && this.email.includes("@");
  }
}

// Implements проверяет контракт, но НЕ добавляет типы автоматически
// Класс должен реализовать все методы/свойства интерфейса

Наследование (extends)

class Animal {
  constructor(public name: string) {}

  move(distance: number): void {
    console.log(`${this.name} moved ${distance}m`);
  }
}

class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name); // Обязательный вызов super
  }

  bark: void {
    console.log("Woof!");
  }

  // Переопределение метода
  override move(distance: number): void {
    console.log("Running...");
    super.move(distance); // Вызов родительского метода
  }
}

const dog = new Dog("Rex", "Shepherd");
dog.bark;     // "Woof!"
dog.move(10);   // "Running..." + "Rex moved 10m"

noImplicitOverride

// tsconfig.json: "noImplicitOverride": true
class Child extends Animal {
  // Без override — ошибка!
  move(distance: number): void {} // Error: must use 'override'

  // Правильно:
  override move(distance: number): void {
    super.move(distance);
  }
}

Static Members

class MathUtils {
  static PI = 3.14159;

  static sum(a: number, b: number): number {
    return a + b;
  }

  static random(min: number, max: number): number {
    return Math.floor(Math.random * (max - min + 1)) + min;
  }
}

MathUtils.PI;          // 3.14159
MathUtils.sum(1, 2);   // 3

// Static блоки (ES2022+)
class Config {
  static instance: Config;
  static {
    Config.instance = new Config();
  }
}

Singleton через static

class Database {
  private static instance: Database;

  private constructor(private connectionString: string) {}

  static getInstance: Database {
    if (!Database.instance) {
      Database.instance = new Database("postgres://localhost:5432/db");
    }
    return Database.instance;
  }

  query(sql: string): unknown {
    console.log(`Executing: ${sql}`);
    return ;
  }
}

const db = Database.getInstance;
db.query("SELECT * FROM users");
// new Database("..."); // Ошибка! Constructor is private

Getters и Setters

class Temperature {
  private _celsius: number;

  constructor(celsius: number) {
    this._celsius = celsius;
  }

  // Getter
  get fahrenheit: number {
    return this._celsius * 9 / 5 + 32;
  }

  // Setter с валидацией
  set fahrenheit(value: number) {
    this._celsius = (value - 32) * 5 / 9;
  }

  get celsius: number {
    return this._celsius;
  }

  set celsius(value: number) {
    if (value < -273.15) {
      throw new Error("Temperature below absolute zero");
    }
    this._celsius = value;
  }
}

const temp = new Temperature(100);
console.log(temp.fahrenheit); // 212
temp.fahrenheit = 32;
console.log(temp.celsius);    // 0

Класс как тип

class User {
  constructor(public name: string, public age: number) {}
}

// User — и конструктор, и тип одновременно
const user: User = new User("Alice", 30);

// Структурная типизация — объект тоже подходит!
const fakeUser: User = { name: "Bob", age: 25 }; // OK, если структура совпадает

// typeof класса — тип конструктора
function createInstance(Cls: typeof User, name: string, age: number): User {
  return new Cls(name, age);
}

// Или через конструкторную сигнатуру
function create(Cls: new (name: string, age: number) => User): User {
  return new Cls("test", 0);
}

Generic Classes

class Result<T, E = Error> {
  private constructor(
    private readonly value: T | null,
    private readonly error: E | null
  ) {}

  static ok<T>(value: T): Result<T, never> {
    return new Result(value, null);
  }

  static err<E>(error: E): Result<never, E> {
    return new Result(null, error);
  }

  isOk: boolean {
    return this.error === null;
  }

  unwrap: T {
    if (this.value === null) throw new Error("Called unwrap on error");
    return this.value;
  }

  unwrapOr(defaultValue: T): T {
    return this.value ?? defaultValue;
  }
}

const success = Result.ok(42);
const failure = Result.err(new Error("Not found"));

console.log(success.unwrap);       // 42
console.log(failure.unwrapOr(0));    // 0

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

  1. Забыть объявить свойство — в TS свойства нужно объявлять до использования
class Bad {
  constructor(name: string) {
    this.name = name; // Ошибка! Property 'name' does not exist
  }
}

// Правильно: parameter property или объявление
class Good {
  constructor(public name: string) {} // Parameter property
}
  1. strictPropertyInitialization — все свойства должны быть инициализированы
class User {
  name: string; // Ошибка! Not initialized
  // Решения:
  name: string = "";         // Значение по умолчанию
  name!: string;             // Definite assignment assertion (осторожно!)
  name: string | undefined;  // Явно допустить undefined
}
  1. this в callback — потеря контекста
class Counter {
  count = 0;

  // Проблема: this потеряется в callback
  increment {
    this.count++;
  }

  // Решение: arrow function property
  increment = () => {
    this.count++;
  };
}
  1. implements не добавляет типы — класс сам должен всё объявить
  2. Структурная типизация — объект с нужной структурой совместим с классом

Практика

  1. Создайте класс Stack<T> с методами push, pop, peek, size
  2. Реализуйте Singleton-паттерн для класса Logger
  3. Создайте класс User с implements Serializable и Validatable
  4. Напишите generic класс Result<T, E> с методами ok, err, unwrap
  5. Используйте getter/setter для валидации данных

Связанные темы

Ресурсы