Две стороны одной медали: Функциональный и ООП подход в 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');
}