Programmation fonctionnelle en TypeScript : niveau avancé
La programmation fonctionnelle (PF) est un paradigme qui, ces dernières années, conquiert activement le monde de JavaScript et TypeScript. Pour les développeurs déjà familiers avec les bases (fonctions pures, immuabilité des données), une nouvelle couche de possibilités s'ouvre : le travail avec les monades, les foncteurs, les applicatifs et les compositions complexes. Dans cet article, nous allons analyser les concepts avancés de la PF en TypeScript avec des exemples pratiques.
1. Immuabilité et partage structurel de données
L'immuabilité (immuabilité) est la pierre angulaire de la PF. En TypeScript, nous pouvons garantir l'immuabilité au niveau des types à l'aide de readonly et ReadonlyArray. Cependant, pour travailler efficacement avec de grandes structures de données (par exemple, le store Redux), on utilise le partage structurel (structural sharing) — des bibliothèques comme Immer ou Immutable.js.
Considérons un exemple utilisant readonly et une fonction pour mettre à jour un objet imbriqué sans mutation :
type User = {
readonly id: number;
readonly name: string;
readonly address: {
readonly city: string;
readonly zip: string;
};
};
const updateUserCity = (user: User, newCity: string): User => ({
...user,
address: {
...user.address,
city: newCity,
},
});
const user: User = { id: 1, name: 'Alice', address: { city: 'NYC', zip: '10001' } };
const updatedUser = updateUserCity(user, 'LA');
console.log(user.address.city); // NYC (l'original n'a pas changé)
console.log(updatedUser.address.city); // LA
Notez : nous créons un nouvel objet, mais nous réutilisons les parties inchangées (par exemple, id et name). C'est ce qu'on appelle le partage structurel manuel. Pour les imbrications profondes, il est préférable d'utiliser des bibliothèques.
2. Foncteurs et leur implémentation
Un foncteur est un conteneur qui implémente la méthode map, permettant d'appliquer une fonction au contenu du conteneur sans violer sa structure. En TypeScript, un foncteur typique est Array (le tableau). Mais nous pouvons créer notre propre foncteur, par exemple Maybe, pour éviter les erreurs avec null et undefined.
Implémentons un foncteur simple Maybe :
class Maybe<T> {
private constructor(private value: T | null) {}
static of<U>(val: U | null | undefined): Maybe<U> {
return new Maybe(val ?? null);
}
map<U>(fn: (val: T) => U): Maybe<U> {
if (this.value === null) {
return Maybe.of<U>(null);
}
return Maybe.of(fn(this.value));
}
getOrElse(defaultValue: T): T {
return this.value !== null ? this.value : defaultValue;
}
}
// Utilisation
const result = Maybe.of('hello')
.map(s => s.toUpperCase())
.map(s => s + ' world')
.getOrElse('fallback');
console.log(result); // "HELLO WORLD"
const empty = Maybe.of<string>(null)
.map(s => s.toUpperCase())
.getOrElse('default');
console.log(empty); // "default"
Le foncteur Maybe permet d'enchaîner des appels de fonctions en toute sécurité, sans se soucier de null. Chaque map retourne un nouveau Maybe, préservant le contexte.
3. Monades : chaînes d'opérations avec effets de bord
Une monade est un foncteur qui implémente également la méthode flatMap (souvent appelée bind ou chain). Elle permet de "déballer" les conteneurs imbriqués. Un exemple classique de monade est Promise ou Array. Considérons la monade Either pour la gestion des erreurs sans exceptions.
type Either<L, R> = Left<L> | Right<R>;
class Left<L> {
readonly value: L;
constructor(value: L) { this.value = value; }
isLeft(): this is Left<L> { return true; }
isRight(): this is Right<ne