Programmation fonctionnelle en TypeScript : niveau avancé
\n\nLa programmation fonctionnelle (PF) est un paradigme qui considère le calcul comme l'évaluation de fonctions mathématiques et évite les changements d'état et les données mutables. TypeScript, étant une surcouche de JavaScript, fournit un système de types puissant qui permet d'implémenter de nombreux concepts de la PF à un nouveau niveau de sécurité et d'expressivité. Dans cet article, nous examinerons les techniques avancées : de l'immutabilité stricte au travail avec les monades et les foncteurs.
\n\nImmutabilité et travail avec des structures immuables
\n\nL'immutabilité (caractère immuable) est la pierre angulaire de la PF. En TypeScript, nous pouvons la garantir au niveau des types à l'aide des modificateurs readonly et des types utilitaires.
// Immuabilité profonde avec ReadonlyDeep\ntype DeepReadonly<T> = {\n readonly [P in keyof T]: DeepReadonly<T[P]>;\n};\n\ninterface User {\n name: string;\n address: { city: string; zip: number };\n}\n\nconst user: DeepReadonly<User> = {\n name: "Alice",\n address: { city: "Moscow", zip: 101000 }\n};\n\n// user.address.city = "SPb"; // Erreur ! Propriété en lecture seule\n\nPour mettre à jour les données en toute sécurité, utilisez des fonctions pures qui retournent de nouveaux objets :
\n\nfunction updateZip(user: DeepReadonly<User>, newZip: number): DeepReadonly<User> {\n return { ...user, address: { ...user.address, zip: newZip } };\n}\n\nconst updatedUser = updateZip(user, 190000);\n\nComposition de fonctions et curryfication
\n\nLa composition permet de construire des transformations complexes à partir de fonctions simples. En TypeScript, à l'aide des génériques, nous pouvons créer des combinateurs typés de manière sécurisée.
\n\n// Type pour la composition de deux fonctions\ntype Compose<A, B, C> = (f: (b: B) => C, g: (a: A) => B) => (a: A) => C;\n\nconst compose: Compose<any, any, any> = (f, g) => (x) => f(g(x));\n\n// Exemple : transformer une chaîne en nombre et l'incrémenter de 1\nconst toNumber = (s: string): number => parseInt(s, 10);\nconst increment = (n: number): number => n + 1;\n\nconst parseAndIncrement = compose(increment, toNumber);\nconsole.log(parseAndIncrement("5")); // 6\n\nLa curryfication transforme une fonction à plusieurs arguments en une séquence de fonctions à un seul argument. Cela rend la composition plus flexible :
\n\n// Fonction de curryfication universelle avec types\nfunction curry<T1, T2, R>(fn: (a: T1, b: T2) => R): (a: T1) => (b: T2) => R {\n return (a: T1) => (b: T2) => fn(a, b);\n}\n\nconst add = (a: number, b: number): number => a + b;\nconst curriedAdd = curry(add);\nconst add5 = curriedAdd(5);\nconsole.log(add5(3)); // 8\n\nFoncteurs et monades : travailler avec des contextes
\n\nUn foncteur est une structure de données qui prend en charge l'opération map. En TypeScript, un tableau, une Promise ou un type personnalisé peuvent être des foncteurs.
// Foncteur Option (Maybe) pour travailler en toute sécurité avec null/undefined\ninterface Option<T> {\n map<U>(fn: (value: T) => U): Option<U>;\n getOrElse(defaultValue: T): T;\n}\n\nclass Some<T> implements Option<T> {\n constructor(private value: T) {}\n map<U>(fn: (value: T) => U): Option<U> {\n return new Some(fn(this.value));\n }\n getOrElse(_: T): T {\n return this.value;\n }\n}\n\nclass None<T> implements Option<T> {\n map<U>(_: (value: T) => U): Option<U> {\n return new None<U>();\n }\n getOrElse(defaultValue: T): T {\n return defaultValue;\n }\n}\n\n// Utilisation\nconst maybeNumber: Option<number> = new Some(42);\nconst result = maybeNumber.map(x => x * 2).getOrElse(0); // 84\n\nUne monade étend le foncteur avec la méthode flatMap (ou bind