TypeScript: Функциональный или ООП — что выбрать в 2024?

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Две стороны одной медали: Функциональный и ООП подход в TypeScript

Мир TypeScript — это удивительное место, где смешиваются лучшие практики из разных миров. Вы когда-нибудь задумывались, почему одни разработчики пишут код, используя классы и наследование, а другие — чистые функции и композицию? И главное: какой подход принесет больше пользы вашему проекту?

В этой статье мы не будем спорить, что лучше. Мы разберем функциональное программирование против объектно-ориентированного в TypeScript на реальных примерах. Вы узнаете, когда стоит использовать классы, а когда — функции высшего порядка, и как найти золотую середину в современной разработке.

Что такое функциональное программирование в TypeScript?

Функциональное программирование (ФП) — это парадигма, где основным строительным блоком являются чистые функции. Они не изменяют внешнее состояние и всегда возвращают один и тот же результат для одних и тех же входных данных. TypeScript отлично поддерживает ФП благодаря строгой типизации и возможностям работы с функциями как с данными.

Основные принципы ФП в TypeScript

  • Иммутабельность — данные не изменяются, а создаются заново
  • Чистые функции — никаких побочных эффектов
  • Композиция — собираем сложное поведение из простых функций
  • Функции высшего порядка — функции, принимающие или возвращающие другие функции
// Пример чистых функций и иммутабельности в TypeScript
type User = {
  readonly id: number;
  readonly name: string;
  readonly age: number;
};

const addYear = (user: User): User => ({
  ...user,
  age: user.age + 1
});

const users: User[] = [
  { id: 1, name: 'Alice', age: 25 },
  { id: 2, name: 'Bob', age: 30 }
];

// Никаких мутаций — создаем новый массив
const updatedUsers = users.map(addYear);
console.log(users[0].age); // 25 (оригинал не изменился)
console.log(updatedUsers[0].age); // 26

Обратите внимание на readonly — это мощный инструмент TypeScript, который помогает соблюдать иммутабельность на уровне типов.

Что такое объектно-ориентированное программирование в TypeScript?

ООП строится вокруг объектов, которые объединяют данные и поведение. TypeScript добавляет в JavaScript классическое наследование, модификаторы доступа и интерфейсы, делая ООП еще более выразительным.

Основные принципы ООП в TypeScript

  • Инкапсуляция — скрытие внутреннего состояния через private и protected
  • Наследование — создание иерархий классов
  • Полиморфизм — единый интерфейс для разных реализаций
  • Абстракция — работа через интерфейсы, а не конкретные классы
// Пример ООП подхода в TypeScript
abstract class Animal {
  constructor(protected name: string) {}

  abstract makeSound(): string;

  public introduce(): string {
    return `I am ${this.name}, and I say ${this.makeSound()}`;
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name);
  }

  public makeSound(): string {
    return 'Woof!';
  }
}

class Cat extends Animal {
  constructor(name: string) {
    super(name);
  }

  public makeSound(): string {
    return 'Meow!';
  }
}

const animals: Animal[] = [new Dog('Rex'), new Cat('Whiskers')];
animals.forEach(a => console.log(a.introduce()));
// I am Rex, and I say Woof!
// I am Whiskers, and I say Meow!

Здесь abstract и protected — это фишки TypeScript, которых нет в обычном JavaScript. Они позволяют строить строгую архитектуру прямо на уровне типов.

Сравнение подходов: когда что использовать?

Давайте разберем несколько сценариев и посмотрим, какой подход лучше подходит в каждом случае.

Сценарий 1: Работа с состоянием

Если у вас есть сложное состояние, которое часто меняется, ООП может быть более интуитивным. Но если вы хотите предсказуемости — выбирайте ФП.

// ООП подход с состоянием
class Counter {
  private count: number = 0;

  increment(): void {
    this.count++;
  }

  getValue(): number {
    return this.count;
  }
}

// Функциональный подход
const createCounter = (initial: number = 0) => {
  let count = initial;
  return {
    increment: (): number => ++count,
    getValue: (): number => count
  };
};

// TypeScript позволяет сузить сигнатуру
type CounterFP = {
  increment: () => number;
  getValue: () => number;
};

Сценарий 2: Обработка данных

Для трансформации данных (мапы, фильтры, редьюсы) функциональный подход в TypeScript — это естественный выбор. Он лаконичнее и безопаснее.

// Функциональный подход — цепочки методов
interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
}

const applyDiscount = (products: Product[], discount: number): Product[] =>
  products
    .filter(p => p.price > 100)
    .map(p => ({ ...p, price: p.price * (1 - discount) }))
    .sort((a, b) => a.price - b.price);

// ООП подход — класс с методами
class ProductManager {
  constructor(private products: Product[]) {}

  applyDiscount(discount: number): Product[] {
    const filtered = this.products.filter(p => p.price > 100);
    const discounted = filtered.map(p => ({ ...p, price: p.price * (1 - discount) }));
    return discounted.sort((a, b) => a.price - b.price);
  }
}

Паттерны проектирования в TypeScript: гибридный подход

Современная разработка на TypeScript редко использует что-то одно в чистом виде. Лучшие проекты комбинируют ООП и ФП, беря лучшее от обоих миров.

Стратегия (Strategy) с функциями

// Функциональная реализация паттерна Стратегия
type PaymentStrategy = (amount: number) => string;

const creditCard: PaymentStrategy = (amount) =>
  `Paid $${amount} via Credit Card`;

const paypal: PaymentStrategy = (amount) =>
  `Paid $${amount} via PayPal`;

const processPayment = (amount: number, strategy: PaymentStrategy): string =>
  strategy(amount);

console.log(processPayment(100, creditCard));
console.log(processPayment(200, paypal));

Фабрика (Factory) с классами

// Классическая фабрика с TypeScript
interface Logger {
  log(message: string): void;
}

class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(`[Console] ${message}`);
  }
}

class FileLogger implements Logger {
  log(message: string): void {
    // Имитация записи в файл
    console.log(`[File] ${message}`);
  }
}

class LoggerFactory {
  static createLogger(type: 'console' | 'file'): Logger {
    switch (type) {
      case 'console':
        return new ConsoleLogger();
      case 'file':
        return new FileLogger();
      default:
        throw new Error('Unknown logger type');
    }                

Блоги

Рекомендаций по книгам