POO vs Fonctionnel — 04 — L'approche hybride en TypeScript et Python

Comment TypeScript et Python combinent POO et programmation fonctionnelle dans les mêmes projets. React, dataclasses, functools, règles de décision pratiques.

04 — L'approche hybride en TypeScript et Python

Ce que tu vas apprendre

  • Comment TypeScript supporte les deux paradigmes nativement
  • Les outils fonctionnels de Python (functools, itertools, dataclasses, générateurs)
  • Le cas React : pourquoi les composants fonctionnels ont remplacé les classes
  • Des règles concrètes pour choisir le bon paradigme dans un même projet

Prérequis


TypeScript : conçu pour les deux

TypeScript hérite de JavaScript, qui a intégré les fonctionnalités fonctionnelles dès les premières versions (fonctions de première classe, closures, Array.prototype.map). ES6 a ajouté les classes. TypeScript ajoute les types statiques sur les deux.

En pratique, un projet TypeScript typique mélange :

typescript// Classes pour le domaine (POO)
class UserService {
  constructor(private readonly userRepo: UserRepository) {}

  async createUser(email: string, name: string): Promise<User> {
    const existingUser = await this.userRepo.findByEmail(Email.create(email));
    if (existingUser) throw new Error("Email déjà utilisé");

    const user = User.create(Email.create(email), name);
    await this.userRepo.save(user);
    return user;
  }
}

// Fonctions pures pour les transformations (FP)
const formatUserList = (users: User[]): UserDTO[] =>
  users
    .filter(u => u.isActive)
    .map(u => ({ id: u.id.toString(), email: u.email.toString(), name: u.name }))
    .sort((a, b) => a.name.localeCompare(b.name));

// Types algébriques (FP) pour la gestion d'erreurs
type AuthResult =
  | { success: true; token: string; user: User }
  | { success: false; reason: "invalid_credentials" | "account_locked" | "not_found" };

TypeScript et les types algébriques

Les types union discriminés de TypeScript permettent d'exprimer des états impossibles à l'aide de types — une technique centrale du FP :

typescript// Sans type algébrique — les deux champs peuvent coexister (bug possible)
type Response = {
  data?: User;
  error?: string;
};

// Avec type algébrique — exactement l'un ou l'autre
type Response =
  | { status: "success"; data: User }
  | { status: "error"; error: string };

function handleResponse(res: Response): void {
  if (res.status === "success") {
    console.log(res.data.name); // TypeScript sait que data existe ici
  } else {
    console.error(res.error); // TypeScript sait que error existe ici
  }
}

Python : multi-paradigme par construction

Python n'a jamais choisi. Guido van Rossum a construit un langage pragmatique qui supporte les deux styles sans forcer le développeur.

Les outils fonctionnels de Python

pythonimport functools
import itertools
from typing import Callable, TypeVar, Iterator, Iterable

T = TypeVar('T')

# functools.reduce — agrégation
from functools import reduce

totals = reduce(lambda acc, x: acc + x, [1, 2, 3, 4, 5], 0)  # 15

# functools.partial — application partielle
from functools import partial

def power(base: int, exponent: int) -> int:
    return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)

print(square(5))  # 25
print(cube(3))    # 27

# functools.lru_cache — mémoïsation (FP pur : même input → même output en cache)
from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# itertools — opérations paresseuses sur des itérables
import itertools

numbers = range(1, 1000000)  # Pas encore en mémoire

# Paresseux — ne calcule que ce qui est consommé
evens = itertools.islice(filter(lambda n: n % 2 == 0, numbers), 10)
print(list(evens))  # [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

# Chaînage d'itérables
all_items = itertools.chain([1, 2, 3], [4, 5, 6], [7, 8, 9])
grouped = itertools.groupby(sorted([3, 1, 2, 3, 1, 2]), key=lambda x: x)

Dataclasses : Value Objects immuables (FP) dans Python OOP

pythonfrom dataclasses import dataclass, field, replace
from typing import List

@dataclass(frozen=True)
class Address:
    street: str
    city: str
    postal_code: str

@dataclass(frozen=True)
class OrderItem:
    product_id: str
    quantity: int
    unit_price: int  # en centimes

    @property
    def subtotal(self) -> int:
        return self.quantity * self.unit_price

@dataclass
class Order:  # Mutable — c'est une entité
    id: str
    items: List[OrderItem] = field(default_factory=list)
    status: str = "pending"
    shipping_address: Address = None

    def add_item(self, item: OrderItem) -> None:
        self._items = [*self.items, item]  # Nouveau tuple — items inchangé

    @property
    def total(self) -> int:
        return sum(item.subtotal for item in self.items)

Le cas React : pourquoi le fonctionnel a gagné

React est l'exemple le plus documenté de migration d'un framework OOP vers le paradigme fonctionnel.

En 2013, React utilisait des class components :

typescript// React pré-hooks (2013-2019) — POO
class Counter extends React.Component<{}, { count: number }> {
  constructor(props: {}) {
    super(props);
    this.state = { count: 0 };
    this.increment = this.increment.bind(this); // Boilerplate
  }

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

  componentDidMount(): void {
    document.title = `Compteur: ${this.state.count}`;
  }

  componentDidUpdate(): void {
    document.title = `Compteur: ${this.state.count}`;
  }

  render(): React.ReactNode {
    return (
      <button onClick={this.increment}>
        Clics : {this.state.count}
      </button>
    );
  }
}

En 2019, React 16.8 introduit les hooks. Le même composant en fonctionnel :

typescript// React avec hooks (2019+) — fonctionnel
function Counter(): React.ReactNode {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `Compteur: ${count}`;
  }, [count]);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Clics : {count}
    </button>
  );
}

Les raisons du changement, documentées par l'équipe React :

  • Réutilisation de logique : les hooks permettent d'extraire et partager de la logique avec état (useFetch, useDebounce) sans HOC ni render props
  • Classes confuses : this en JavaScript se comporte différemment selon le contexte — source de bugs fréquents
  • Composition naturelle : useState + useEffect + useMemo se composent comme des fonctions

"Classes confuse both people and machines." — Dan Abramov, React team, Hooks Introduction (2018)


Règles de décision dans un projet hybride

Voici les règles appliquées dans les projets TypeScript/Python modernes :

Utilise des classes (POO) pour :

✓ Les entités du domaine (User, Order, Product)
  → ont un cycle de vie, des invariants à protéger

✓ Les services (UserService, PaymentService)
  → ont des dépendances injectées (repo, API client)

✓ Les erreurs (ValidationError, NotFoundError)
  → hiérarchie avec instanceof

✓ Les classes abstraites et interfaces
  → polymorphisme pour l'extensibilité

Utilise des fonctions (FP) pour :

✓ Les transformations de données
  → mapper, filtrer, agréger des collections

✓ Les utilitaires purs
  → formatDate, parseSlug, toSlug

✓ Les handlers HTTP (Express, Elysia, FastAPI)
  → request in, response out — fonctions pures

✓ La logique de validation
  → prédicats composables

✓ Les pipelines de données (ETL, reporting)
  → lisibilité, testabilité maximale

La règle simple

Un objet qui a un état qui change au fil du temps → classe.

Une transformation de données sans état → fonction.

UserService a userRepo comme dépendance injectée → classe. formatUser(user: User): UserDTO ne dépend que de ses arguments → fonction.

En pratique : si tu te retrouves à ajouter des private devant chaque propriété d'une fonction utilitaire, c'est que la classe est inutile. Si tu te retrouves à passer les mêmes arguments à 5 fonctions liées, c'est que l'objet est justifié.


Exemple concret : une API REST hybride

typescript// Elysia (TypeScript) — handlers fonctionnels, services en classes

// Fonctions pures — validation et formatage
const parseOrderId = (raw: string): Result<OrderId> => {
  try {
    return { ok: true, value: OrderId.create(raw) };
  } catch {
    return { ok: false, error: "ID de commande invalide" };
  }
};

const formatOrder = (order: Order): OrderDTO => ({
  id: order.id.toString(),
  status: order.status.toString(),
  total: order.total.amount / 100,
  currency: order.total.currency,
  items: order.items.map(item => ({
    productId: item.productId,
    quantity: item.quantity.value,
    unitPrice: item.unitPrice.amount / 100,
  })),
});

// Classe — service avec dépendances injectées
class OrderService {
  constructor(
    private readonly orderRepo: OrderRepository,
    private readonly eventBus: EventBus
  ) {}

  async getOrder(rawId: string): Promise<Result<OrderDTO>> {
    const idResult = parseOrderId(rawId);          // fonction pure
    if (!idResult.ok) return idResult;

    const order = await this.orderRepo.findById(idResult.value);
    if (!order) return { ok: false, error: "Commande introuvable" };

    return { ok: true, value: formatOrder(order) }; // fonction pure
  }
}

// Handler HTTP — fonction pure
const getOrderHandler = (service: OrderService) =>
  async (ctx: Context): Promise<Response> => {
    const result = await service.getOrder(ctx.params.id);
    return result.ok
      ? ctx.json(result.value)
      : ctx.json({ error: result.error }, 404);
  };

En Python avec FastAPI :

pythonfrom fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

# Fonction pure — transformation de données
def format_order(order: Order) -> dict:
    return {
        "id": order.id.value,
        "status": order.status,
        "total": order.total.amount / 100,
        "items": [
            {
                "product_id": item.product_id,
                "quantity": item.quantity.value,
                "unit_price": item.unit_price.amount / 100,
            }
            for item in order.items
        ],
    }

# Classe — service avec dépendances
class OrderService:
    def __init__(self, order_repo: OrderRepository) -> None:
        self._repo = order_repo

    async def get_order(self, order_id: str) -> dict:
        order = await self._repo.find_by_id(OrderId(order_id))
        if not order:
            raise HTTPException(status_code=404, detail="Commande introuvable")
        return format_order(order)  # fonction pure

# Handler HTTP — fonction pure (FastAPI)
@app.get("/orders/{order_id}")
async def get_order(order_id: str, service: OrderService = Depends(get_order_service)):
    return await service.get_order(order_id)

Résumé

Contexte TypeScript Python
Domaine métier Classes + Value Objects @dataclass + classes
Transformations map/filter/reduce + fonctions fléchées List comprehensions + functools
Gestion d'erreurs Result type (union discriminée) Exceptions ou tuples (value, error)
UI Composants fonctionnels + hooks
API handlers Fonctions pures Fonctions FastAPI/Flask
Tests Fonctions pures — pas de mock pytest + fonctions pures

La vraie force de TypeScript et Python : tu n'as pas à choisir. La même codebase peut avoir des classes pour le domaine et des pipelines fonctionnels pour les transformations, sans friction.

Étape suivante : 05 — Patterns fonctionnels dans un projet OOP — comment intégrer le FP progressivement dans un code existant.


Sources

  • Abramov, D. (2018). Introducing Hooks. React Blog. reactjs.org.
  • Van Rossum, G. (2009). Python's History. Artima Developer.
  • Klabnik, S., & Nichols, C. (2019). The Rust Programming Language. No Starch Press. (pour le modèle de propriété et l'immutabilité)
  • Wlaschin, S. (2018). Domain Modeling Made Functional. Pragmatic Bookshelf.
  • Fowler, M. (2004). Inversion of Control Containers and the Dependency Injection pattern. martinfowler.com.

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.