Effect-ts pour TypeScript : guide complet avec exemples

Formateur Python en ligne pour débutants

Apprenez Python facilement sans surcharge théorique. Résolvez des tâches pratiques avec vérification automatique et écrivez du code directement dans le navigateur.

Start Course

Qu'est-ce qu'Effect-ts et pourquoi l'utiliser ?

Effect-ts est une bibliothèque puissante pour TypeScript qui apporte les concepts de la programmation fonctionnelle au langage, permettant d'écrire un code pur, composable et typé de manière sécurisée. Elle résout l'un des problèmes majeurs du développement en TypeScript (et JavaScript en général) : la gestion des effets de bord (side effects).

Dans la programmation impérative traditionnelle, les effets de bord (appels réseau, fichiers, bases de données, génération de nombres aléatoires, gestion du temps) sont dispersés dans tout le code, ce qui le rend difficile à tester, déboguer et analyser. Effect-ts propose une approche élégante : tous les effets de bord sont décrits comme des données (valeurs) que l'on peut combiner, transformer et exécuter dans un environnement contrôlé.

Les principaux avantages d'Effect-ts :

  • Sécurité des types : Le compilateur TypeScript vérifie les types d'erreurs et de résultats au niveau des types.
  • Composabilité : Les effets se combinent facilement avec les fonctions map, flatMap, zip et autres.
  • Gestion des erreurs : Les erreurs sont traitées comme une partie du type d'effet, et non via des exceptions.
  • Testabilité : Les dépendances (clients HTTP, bases de données) peuvent être facilement remplacées.
  • Concurrence : Prise en charge intégrée de l'asynchrone, du parallélisme et de l'annulation d'opérations.
  • Prévisibilité : Le code devient plus déclaratif et plus facile à comprendre.

Effect-ts est particulièrement utile dans les grands projets où la fiabilité, la testabilité et l'évolutivité sont essentielles. Il est utilisé dans le développement backend (Node.js), frontend (React) et même dans les applications mobiles.

Installation

Pour installer Effect-ts dans votre projet, utilisez npm ou yarn :

npm install effect

ou

yarn add effect

Assurez-vous également d'avoir TypeScript version 4.7 ou supérieure :

npm install typescript --save-dev

Pour travailler avec Effect-ts, vous devrez configurer tsconfig.json en activant le mode strict (strict: true) et, idéalement, target: "ES2020" ou supérieur.

Démarrage rapide avec un exemple de code

Écrivons un programme simple qui lit le nom d'un utilisateur depuis la ligne de commande et affiche un message de bienvenue. Cela illustre le concept de base d'Effect-ts : la description d'un effet et son exécution.

import { Effect, Console } from "effect"

// Description de l'effet : demande du nom
const askName: Effect.Effect<string> = Effect.sync(() => {
  // Dans une application réelle, il y aurait une saisie clavier
  return "Alexandre"
})

// Description de l'effet : affichage du message
const greet = (name: string): Effect.Effect<void> =>
  Console.log(`Bonjour, ${name} !`)

// Combinaison des effets
const program: Effect.Effect<void> = askName.pipe(
  Effect.flatMap(greet)
)

// Exécution du programme
Effect.runSync(program)
// Affichage : Bonjour, Alexandre !

Dans cet exemple :

  • Effect.sync — crée un effet à partir d'une fonction synchrone.
  • Console.log — est un effet pour afficher dans la console.
  • pipe — permet d'appliquer séquentiellement des fonctions à une valeur.
  • flatMap — est utilisé pour exécuter séquentiellement des effets.
  • Effect.runSync — exécute l'effet de manière synchrone (uniquement pour les effets synchrones).

Méthodes et fonctions principales d'Effect-ts

1. Effect.sync

Fonction : Crée un effet qui encapsule une fonction synchrone. La fonction est exécutée paresseusement (uniquement lors du lancement de l'effet).

Paramètres :

  • f: () => A — fonction synchrone retournant une valeur de type A.

Exemple :

import { Effect } from "effect"

const getRandomInt: Effect.Effect<number> = Effect.sync(() => {
  return Math.floor(Math.random() * 100)
})

// Exécution
const result = Effect.runSync(getRandomInt)
console.log(result) // Affiche un nombre aléatoire entre 0 et 99

2. Effect.async

Fonction : Crée un effet à partir d'une fonction asynchrone basée sur des callbacks. Utile pour intégrer des API existantes basées sur des callbacks.

Paramètres :

  • register: (resume: (result: Effect.Effect<A, E>) => void) => void — fonction qui reçoit un callback resume pour signaler la fin de l'opération asynchrone.

Exemple :

import { Effect } from "effect"

const readFile = (path: string): Effect.Effect<string, Error> =>
  Effect.async<string, Error>((resume) => {
    fs.readFile(path, "utf8", (err, data) => {
      if (err) {
        resume(Effect.fail(new Error(err.message)))
      } else {
        resume(Effect.succeed(data))
      }
    })
  })

3. Effect.map

Fonction : Transforme la valeur de succès d'un effet à l'aide d'une fonction pure. Similaire à Array.map.

Paramètres :

  • f: (a: A) => B — fonction de transformation.

Exemple :

import { Effect } from "effect"

const getAge: Effect.Effect<number> = Effect.sync(() => 25)
const ageNextYear: Effect.Effect<number> = Effect.map(getAge, (age) => age + 1)

Effect.runSync(ageNextYear) // 26

4. Effect.flatMap

Fonction : Enchaîne deux effets de manière séquentielle. Le second effet dépend de la valeur de sortie du premier.

Paramètres :

  • f: (a: A) => Effect.Effect<B, E, R> — fonction qui prend la valeur de succès du premier effet et retourne un nouvel effet.

Exemple :

import { Effect } from "effect"

const getUser: Effect.Effect<{ id: number; name: string }> = Effect.sync(() => ({
  id: 1,
  name: "Marie",
}))

const getOrders = (userId: number): Effect.Effect<string[]> =>
  Effect.sync(() => [`Commande ${userId}-1`, `Commande ${userId}-2`])

const program = getUser.pipe(
  Effect.flatMap((user) => getOrders(user.id))
)

Effect.runSync(program) // ["Commande 1-1", "Commande 1-2"]

5. Effect.zip

Fonction : Combine deux effets en un seul, produisant un tuple des deux résultats. Les effets sont exécutés séquentiellement.

Paramètres :

  • that: Effect.Effect<B, E, R> — second effet à combiner.

Exemple :

import { Effect } from "effect"

const getName: Effect.Effect<string> = Effect.sync(() => "Pierre")
const getAge: Effect.Effect<number> = Effect.sync(() => 30)

const person: Effect.Effect<[string, number]> = Effect.zip(getName, getAge)

Effect.runSync(person) // ["Pierre", 30]

6. Effect.all

Fonction : Exécute une collection d'effets en parallèle (par défaut) ou séquentiellement, et retourne un tableau ou un objet de résultats.

Paramètres :

  • effects: Iterable<Effect.Effect<A, E, R>> | Record<string, Effect.Effect<A, E, R>> — collection d'effets.
  • options?: { concurrency?: number | "unbounded"; discard?: boolean } — options de concurrence.

Exemple :

import { Effect } from "effect"

const fetchUser = Effect.sync(() => ({ id: 1, name: "Sophie" }))
const fetchPosts = Effect.sync(() => ["Article 1", "Article 2"])

const combined = Effect.all([fetchUser, fetchPosts])

Effect.runSync(combined) // [{ id: 1, name: "Sophie" }, ["Article 1", "Article 2"]]

7. Effect.catchAll

Fonction : Gère les erreurs d'un effet en fournissant un effet de remplacement. Similaire à try/catch.

Paramètres :

  • handler: (e: E) => Effect.Effect<A, E2, R> — fonction qui reçoit l'erreur et retourne un effet de remplacement.

Exemple :

import { Effect } from "effect"

const riskyOperation: Effect.Effect<number, Error> = Effect.sync(() => {
  throw new Error("Échec !")
})

const safeOperation = Effect.catchAll(riskyOperation, (error) => {
  console.error(error.message)
  return Effect.succeed(0) // Valeur par défaut
})

Effect.runSync(safeOperation) // 0

8. Effect.catchTag

Fonction : Gère un type d'erreur spécifique (basé sur le champ _tag d'une union discriminée). Permet une gestion fine des erreurs.

Paramètres :

  • tag: string — le tag de l'erreur à capturer.
  • handler: (e: Extract<E, { _tag: K }>) => Effect.Effect<A, E2, R> — gestionnaire pour ce type d'erreur.

Exemple :

import { Effect } from "effect"

type HttpError =
  | { _tag: "NotFound"; message: string }
  | { _tag: "Unauthorized"; message: string }

const fetchData: Effect.Effect<string, HttpError> = Effect.fail({
  _tag: "NotFound",
  message: "Ressource introuvable",
})

const handled = Effect.catchTag(fetchData, "NotFound", (error) => {
  console.log(`Erreur 404 : ${error.message}`)
  return Effect.succeed("Données par défaut")
})

Effect.runSync(handled) // "Données par défaut"

9. Effect.retry

Fonction : Réessaie un effet en cas d'échec, avec des options de stratégie de réessai (délai, nombre de tentatives, etc.).

Paramètres :

  • policy: Schedule<A, E, Out> — une politique de réessai (Schedule).

Exemple :

import { Effect, Schedule } from "effect"

let attempts = 0
const unstableOperation: Effect.Effect<string, Error> = Effect.sync(() => {
  attempts++
  if (attempts < 3) throw new Error("Temporaire")
  return "Succès"
})

const withRetry = Effect.retry(
  unstableOperation,
  Schedule.recurs(3) // Réessaie jusqu'à 3 fois
)

Effect.runSync(withRetry) // "Succès"

10. Effect.timeout

Fonction : Limite la durée d'exécution d'un effet. Si l'effet dépasse le délai, il échoue avec une erreur TimeoutException.

Paramètres :

  • duration: Duration.DurationInput — durée maximale (en millisecondes ou objet Duration).

Exemple :

import { Effect, Duration } from "effect"

const slowOperation: Effect.Effect<string> = Effect.async((resume) => {
  setTimeout(() => {
    resume(Effect.succeed("Terminé"))
  }, 5000) // 5 secondes
})

const withTimeout = Effect.timeout(slowOperation, Duration.seconds(2))

Effect.runPromise(withTimeout).catch(console.error)
// Erreur : TimeoutException après 2 secondes

Gestion des dépendances avec Effect-ts

L'un des aspects les plus puissants d'Effect-ts est la gestion des dépendances via le système de Services et Layers. Cela permet d'injecter des dépendances de manière typée et testable.

Définir un service

import { Context, Effect } from "effect"

// Définition du service
class Logger extends Context.Tag("Logger")<
  Logger,
  { readonly log: (message: string) => Effect.Effect<void> }
>() {}

// Implémentation du service
const LoggerLive = Logger.of({
  log: (message) => Effect.sync(() => console.log(`[LOG] ${message}`)),
})

Utiliser un service

import { Effect } from "effect"

const program = Effect.gen(function* (_) {
  const logger = yield* _(Logger)
  yield* _(logger.log("Application démarrée"))
})

// Exécution avec les dépendances
Effect.runSync(Effect.provideService(program, Logger, LoggerLive))
// Affiche : [LOG] Application démarrée

Conclusion

Effect-ts transforme la façon dont vous écrivez du code TypeScript en rendant les effets de bord explicites, composables et testables. Bien que la courbe d'apprentissage puisse être raide, les avantages en termes de fiabilité et de maintenabilité du code sont considérables, en particulier dans les applications complexes.

Pour aller plus loin, consultez la documentation officielle d'Effect-ts et explorez les nombreux modules disponibles (HTTP, Stream, SQL, etc.).

Recommendations