← Retour au blog
TypeScript

Pourquoi Effect ? Une introduction open-source à la librairie

Pourquoi Effect en TypeScript : explicitation des erreurs et dépendances, testing, résilience, composabilité, concurrence et performance, bien au-delà du Functional Programming.

📅 ✍️ Antoine Coulon
effecttypescriptfunctional-programmingresiliencecomposability

Construire une application sérieuse en TypeScript, c’est accepter de vivre avec une longue liste de problèmes transverses : des erreurs qui se faufilent hors du système de types, des dépendances câblées en dur qui rendent les tests pénibles, une résilience qu’on rajoute à la main service par service, de la concurrence qu’on orchestre à coups de Promise.all et de drapeaux. On finit par traiter ces préoccupations une par une, avec des outils différents et incompatibles entre eux. Effect propose l’inverse : une bibliothèque standard riche qui adresse l’ensemble de ces problèmes dans un modèle unique et composable.

Plutôt que de partir de la solution, mieux vaut partir des problèmes. C’est l’angle de l’introduction « pourquoi Effect » que j’ai rédigée et rendue open-source : elle passe en revue les difficultés concrètes que nous rencontrons tous comme développeurs, puis montre en quoi Effect permet de les résoudre d’une manière élégante et progressive.

Effect n’est pas une librairie de niche FP

Premier malentendu à lever, parce qu’il bloque beaucoup de gens dès le départ : Effect n’est pas une librairie ésotérique réservée aux passionnés de programmation fonctionnelle. C’est avant tout une bibliothèque standard, extrêmement riche et volontairement sans opinion forte, dont l’objectif est de faciliter l’élaboration de solutions complexes.

Elle ne vous demande pas de choisir un camp entre objet et fonctionnel : elle mixe le meilleur des deux mondes. On peut l’aborder progressivement, brique par brique, sans devoir réécrire toute son application ni adopter un vocabulaire mathématique. Le bénéfice se mesure sur le terrain (robustesse, testabilité, lisibilité), pas dans l’adhésion à un paradigme.

Les problèmes qu’Effect adresse

L’intérêt d’Effect tient à ce qu’il rassemble, sous un même type et un même modèle d’exécution, une série de préoccupations qu’on traite d’habitude avec des outils dispersés.

Explicitation : erreurs, dépendances et résultats dans le type

Le cœur du modèle, c’est de rendre explicite ce que TypeScript laisse habituellement dans l’ombre. Une Promise<A> ne vous dit qu’une chose : en cas de succès, vous obtiendrez un A. Elle ne dit rien des erreurs qu’elle peut produire, ni des dépendances dont elle a besoin pour s’exécuter.

Un Effect encode les trois dans sa signature :

import { Effect } from "effect";

// Effect<Success, Error, Requirements>
declare const getUser: (
  id: string
) => Effect.Effect<User, UserNotFound | DbError, Database>;

On lit immédiatement, dans le type, ce que la fonction produit (User), comment elle peut échouer (UserNotFound | DbError) et ce dont elle dépend pour tourner (Database). Les erreurs ne sont plus des exceptions invisibles qui remontent à l’exécution : elles font partie du contrat, et le compilateur vous force à les traiter. Les dépendances ne sont plus importées en dur : elles sont déclarées et fournies au moment de l’exécution.

Testing : l’injection de dépendances par construction

Cette explicitation des dépendances a une conséquence directe sur les tests. Puisqu’un effet déclare ce dont il a besoin (Database, un client HTTP, une horloge…) sans savoir comment ces services sont implémentés, on peut fournir une implémentation réelle en production et une implémentation de test en vérification.

L’injection de dépendances n’est plus un framework qu’on rajoute par-dessus : elle est intégrée au modèle. Tester un morceau de logique métier devient une question de fournir le bon environnement, sans monkey-patching ni mocks fragiles à recâbler à chaque refactoring.

Résilience : retries, timeouts et tolérance aux pannes intégrés

La gestion des défaillances est, elle aussi, traitée nativement. Les patterns de résilience qu’on assemble d’ordinaire à la main (retries avec backoff, timeouts, interruptions) sont des opérateurs de première classe qui se composent directement sur n’importe quel effet :

import { Effect, Schedule } from "effect";

const resilient = getUser("42").pipe(
  Effect.timeout("2 seconds"),
  Effect.retry(Schedule.exponential("100 millis").pipe(Schedule.recurs(3)))
);

On décrit la politique de résilience à côté de la logique, sans la noyer dedans. Et parce que ces opérateurs partagent le même modèle, ils se combinent sans friction au lieu de s’empiler en couches incompatibles.

Composabilité : tout devient une brique réutilisable

C’est sans doute la propriété la plus structurante. Un effet est une valeur : on le manipule, on le combine, on le réutilise comme n’importe quelle autre donnée. Les petites briques se composent en briques plus grandes via pipe, sans imbrication de callbacks ni effets de bord cachés.

Cette uniformité change la façon de construire : au lieu d’écrire des fonctions qui font des choses, on décrit des effets qui représentent des choses à faire, puis on les assemble. La logique de gestion d’erreur, de retry, de logging ou de concurrence se branche sur le même objet, de façon cohérente, partout dans la base de code.

Concurrence et performance

Effect fournit un modèle de concurrence structuré, appuyé sur des fibers légères, qui permet d’exécuter, d’interrompre et de coordonner un grand nombre de tâches sans tomber dans les pièges classiques du parallélisme manuel. Les interruptions sont propres : annuler un effet annule proprement le travail en cours et ses ressources, ce qui est notoirement difficile à obtenir avec des promesses brutes.

Côté performance, ce modèle d’exécution permet de gérer la concurrence et le contrôle du flux de manière fine, là où l’on devrait sinon jongler entre files d’attente, sémaphores et limites de débit improvisées.

Logging et tracing

Enfin, l’observabilité fait partie du package. Le logging et le tracing sont intégrés au modèle d’exécution plutôt que collés par-dessus : on peut instrumenter ses effets, suivre leur déroulement et tracer leurs dépendances sans réécrire son code métier pour l’occasion. L’observabilité devient une propriété de la composition, pas une couche supplémentaire à maintenir.

Une adoption progressive

Le point important, c’est qu’on n’a pas à tout adopter d’un coup. On peut commencer par envelopper un appel risqué dans un effet pour gagner en explicitation sur les erreurs, puis tirer le fil : ajouter une politique de retry ici, une dépendance injectable là, une trace plus loin. Chaque brique apporte une valeur immédiate, et l’ensemble se renforce à mesure qu’on en compose davantage.

C’est ce qui distingue Effect d’un simple utilitaire fonctionnel : ce n’est pas une contrainte qu’on s’impose, mais un socle commun sur lequel on fait converger des préoccupations qu’on traitait jusque-là séparément.

Conclusion

Explicitation, testing, résilience, composabilité, concurrence, performance, logging et tracing : autant de problèmes que nous adressons habituellement avec des outils éparpillés, au prix d’une complexité d’intégration permanente. La promesse d’Effect est de les réunir dans un modèle unique, typé et composable, qui s’adopte progressivement et sans renier l’écosystème TypeScript.

Cette introduction n’est qu’un prélude orienté problèmes. La suite naturelle consiste à entrer dans la pratique, brique par brique : c’est ce que j’explorerai dans les articles à venir.