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 :
thisen JavaScript se comporte différemment selon le contexte — source de bugs fréquents - Composition naturelle :
useState+useEffect+useMemose 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.