Ces derniers temps, j'ai animé plusieurs dojos de code avec mon équipe. On a pris
le Tennis Kata (l'exercice qu'on croit connaître) et on y est revenus séance
après séance, en changeant de contrainte à chaque fois.

Le kata a l'air trivial. Quatre points, un deuce, un avantage, un jeu gagné. On
l'écrit en vingt minutes. C'est précisément ce qui en fait un bon terrain : la
solution évidente marche, passe les tests, et cache pourtant tout ce qui suit.

Ce que ces itérations ont fait remonter m'a confirmé une conviction que je
traîne depuis un moment : **la machine à états est sous-cotée dans les
applications de gestion.** On y modélise des devis, des commandes, des dossiers,
des contrats, tous des objets qui passent leur vie à changer d'état. Et on les
représente presque toujours de la même façon : un champ `statut`, et des tests
sur ce champ dispersés dans toute la codebase.

Le tennis, lui, tient sur un écran. On y voit donc en vingt minutes ce qu'un
back-office met deux ans à révéler.

Et il y a mieux. Séance après séance, l'équipe est allée plus loin que la machine
à états : quand chaque état s'est mis à porter ses propres données, une classe
par état s'est imposée d'elle-même. Couplées, les deux approches font disparaître
des familles entières de bugs : pas les corriger, les rendre inexprimables.

Cet article, c'est ce que j'ai observé pendant ces dojos, condensé. On va écrire
le Tennis Kata trois fois.

## Les règles, pour fixer le vocabulaire

- Les points se comptent **Love, Fifteen, Thirty, Forty**.
- À égalité à Forty, on est à **Deuce**.
- Depuis Deuce, gagner un point donne l'**Advantage**. Le perdre ramène à Deuce.
- Depuis l'Advantage, gagner à nouveau donne le **jeu**.
- À Forty contre moins, gagner un point donne le jeu.

## Premier jet : tout vérifier dans une méthode

L'implémentation qui vient naturellement : deux compteurs, et une méthode qui
traduit ces compteurs en libellé.

_La solution évidente_

**TypeScript**

```ts
class TennisGame {
  private p1 = 0;
  private p2 = 0;

  pointWonBy(player: 'player1' | 'player2'): void {
    if (player === 'player1') this.p1++;
    else this.p2++;
  }

  score(): string {
    const names = ['Love', 'Fifteen', 'Thirty', 'Forty'];

    if (this.p1 === this.p2) {
      return this.p1 >= 3 ? 'Deuce' : `${names[this.p1]}-All`;
    }

    if (this.p1 >= 4 || this.p2 >= 4) {
      const diff = this.p1 - this.p2;
      if (diff === 1) return 'Advantage player1';
      if (diff === -1) return 'Advantage player2';
      return diff > 0 ? 'Game player1' : 'Game player2';
    }

    return `${names[this.p1]}-${names[this.p2]}`;
  }
}
```

**C#**

```csharp
public enum Player { Player1, Player2 }

public sealed class TennisGame
{
    private static readonly string[] Names = ["Love", "Fifteen", "Thirty", "Forty"];

    private int _p1;
    private int _p2;

    public void PointWonBy(Player player)
    {
        if (player == Player.Player1) _p1++;
        else _p2++;
    }

    public string Score()
    {
        if (_p1 == _p2)
            return _p1 >= 3 ? "Deuce" : $"{Names[_p1]}-All";

        if (_p1 >= 4 || _p2 >= 4)
        {
            var diff = _p1 - _p2;
            if (diff == 1) return "Advantage player1";
            if (diff == -1) return "Advantage player2";
            return diff > 0 ? "Game player1" : "Game player2";
        }

        return $"{Names[_p1]}-{Names[_p2]}";
    }
}
```

Ça marche. Les tests passent. C'est la solution que rendent 90 % des candidats,
et honnêtement, sur un kata de vingt minutes, elle est défendable.

Regarde quand même ce que fait `score()` : à **chaque appel**, elle redéduit dans
quel état on se trouve. Égalité ? Au-delà de quatre ? Quel écart ? L'état du jeu
n'est écrit nulle part ; il est recalculé à la volée depuis deux entiers.

Et deux entiers, ça permet beaucoup de choses.

_Ce que le modèle autorise_

**TypeScript**

```ts
const game = new TennisGame();
for (let i = 0; i < 47; i++) game.pointWonBy('player1');

game.score(); // 'Game player1'
```

**C#**

```csharp
var game = new TennisGame();
for (var i = 0; i < 47; i++) game.PointWonBy(Player.Player1);

game.Score(); // "Game player1"
```

Quarante-sept points gagnés d'affilée. Le jeu était fini depuis le quatrième.
`pointWonBy` a continué d'incrémenter sans broncher, et `score()` répond quand
même quelque chose de plausible.

Le modèle ment sur deux plans :

- **Il accepte des points après la fin du jeu.** Aucune méthode ne refuse jamais
  rien.
- **Il représente des scores impossibles.** `p1 = 47` est un `number` parfaitement
  valide. `p1 = -3` aussi.

**Le problème n'est pas cette boucle. Le problème, c'est qu'elle compile.**

## Deuxième jet : la machine à états

Le premier réflexe (et c'est un bon réflexe) est de rendre l'état explicite
au lieu de le redéduire. Un jeu de tennis a quatre états : on joue les points, on
est à Deuce, quelqu'un a l'Advantage, ou le jeu est gagné.

_Les états et leurs enchaînements_

**TypeScript**

```ts
type GameState = 'points' | 'deuce' | 'advantage' | 'game';

const TRANSITIONS: Record<GameState, readonly GameState[]> = {
  points:    ['points', 'deuce', 'game'],
  deuce:     ['advantage'],
  advantage: ['deuce', 'game'],
  game:      [],
};
```

**C#**

```csharp
public enum GameState { Points, Deuce, Advantage, Game }

private static readonly Dictionary<GameState, GameState[]> Transitions = new()
{
    [GameState.Points]    = [GameState.Points, GameState.Deuce, GameState.Game],
    [GameState.Deuce]     = [GameState.Advantage],
    [GameState.Advantage] = [GameState.Deuce, GameState.Game],
    [GameState.Game]      = [],
};
```

Cette table se lit comme un diagramme, et elle dit déjà quelque chose de fort :
**`game` est terminal**. Aucune sortie. Le jeu fini ne mène nulle part.

Le reste de la classe suit :

_L'état devient un champ_

**TypeScript**

```ts
class TennisGame {
  private state: GameState = 'points';
  private p1 = 0;
  private p2 = 0;
  private advantaged?: Player;
  private winner?: Player;

  pointWonBy(player: Player): void {
    if (this.state === 'game') {
      throw new Error('Le jeu est terminé');
    }
    // ... calcule le nouvel état, met à jour les champs concernés
  }
}
```

**C#**

```csharp
public sealed class TennisGame
{
    private GameState _state = GameState.Points;
    private int _p1;
    private int _p2;
    private Player? _advantaged;
    private Player? _winner;

    public void PointWonBy(Player player)
    {
        if (_state == GameState.Game)
            throw new InvalidOperationException("Le jeu est terminé");

        // ... calcule le nouvel état, met à jour les champs concernés
    }
}
```

Ce que tu viens de gagner n'est pas anodin :

- **Le jeu refuse les points après la fin.** Les 47 points de tout à l'heure
  lèvent maintenant une exception au cinquième. C'est le bug le plus visible du
  premier jet, corrigé.
- **Les règles d'enchaînement sont à un seul endroit.** La table *est* la
  spécification, et elle se teste en quelques lignes.
- **`score()` cesse de deviner.** Elle devient un aiguillage sur `state` au lieu
  d'une cascade de comparaisons.

Si tu en es là, tu as sérieusement amélioré la situation. Beaucoup de codebases
ne vont jamais plus loin, et se portent très bien.

## Ce que la machine à états ne règle pas

Reviens sur les champs de la classe. Il y en a maintenant cinq, et regarde
lesquels ont un sens dans quel état :

| Champ | A du sens quand… |
|---|---|
| `p1`, `p2` | on joue les points ; plus rien à dire une fois à Deuce |
| `advantaged` | et seulement si on est à Advantage |
| `winner` | et seulement si le jeu est gagné |
| `state` | toujours |

Deux champs optionnels, deux compteurs qui ne veulent plus rien dire passé
Deuce. Le compilateur, lui, autorise **toutes** les combinaisons : un état
`deuce` avec un `winner` renseigné, un état `game` sans `winner`, un `advantaged`
qui traîne d'un avantage perdu il y a trois points.

Et comme le type dit oui, ton code doit dire non. Partout :

_L'aveu_

**TypeScript**

```ts
score(): string {
  switch (this.state) {
    case 'game':
      if (this.winner === undefined) {
        throw new Error('Jeu gagné sans vainqueur ?!'); // le "?!" en dit long
      }
      return `Game ${this.winner}`;

    case 'advantage':
      if (this.advantaged === undefined) {
        throw new Error('Advantage sans joueur ?!');
      }
      return `Advantage ${this.advantaged}`;

    // ...
  }
}
```

**C#**

```csharp
public string Score() => _state switch
{
    GameState.Game => _winner is null
        ? throw new InvalidOperationException("Jeu gagné sans vainqueur ?!") // le "?!" en dit long
        : $"Game {_winner}",

    GameState.Advantage => _advantaged is null
        ? throw new InvalidOperationException("Advantage sans joueur ?!")
        : $"Advantage {_advantaged}",

    // ...
};
```

Ces `throw` sont des aveux. Tu *sais* qu'un jeu gagné a un vainqueur. Le
compilateur, lui, ne le sait pas, alors tu écris une garde défensive, tu la
testes, tu la maintiens, et tu la répètes partout où un champ optionnel est en
réalité obligatoire *dans cet état-là*.

La machine à états a validé les **transitions**. Elle n'a rien dit sur les
**données**.

## Le déclic : Deuce ne porte aucune donnée

Regarde ce que chaque état sait réellement, et lui seul :

| État | Données propres |
|---|---|
| Points | les deux compteurs |
| Deuce | **rien du tout** |
| Advantage | quel joueur a l'avantage |
| Game | quel joueur a gagné |

Cette ligne « rien du tout » est le cœur du sujet. **Deuce, c'est Deuce.** Il n'y
a rien à savoir de plus. Pourtant, dans la version machine à états, un jeu à
Deuce trimballe quand même deux compteurs, un `advantaged` et un `winner` : quatre
champs pour un état qui n'en a besoin d'aucun.

Et ce n'est pas qu'une affaire de données. Les *opérations* diffèrent aussi :

- Depuis Points, gagner un point mène à Points, à Deuce, ou au jeu.
- Depuis Deuce, gagner un point mène toujours à Advantage. Jamais ailleurs.
- Depuis Advantage, on retourne à Deuce ou on gagne.
- Depuis Game, on ne fait **rien**. C'est fini.

Quand les données **et** les opérations diffèrent d'un état à l'autre, tu n'as
pas un objet avec un champ `state`. Tu as une famille de types. C'est le
**Typestate**.

## Troisième jet : une classe par état

L'idée tient en une phrase : *l'état devient le type*. Les transitions ne
modifient pas un champ, elles retournent une instance d'un autre type.

Commençons par rendre un score impossible… impossible :

_Fini les entiers_

**TypeScript**

```ts
type Player = 'player1' | 'player2';
type PointCount = 'Love' | 'Fifteen' | 'Thirty' | 'Forty';

// Forty est exclu des CLÉS : depuis Forty on ne progresse pas, on gagne.
// Le compilateur refusera donc NEXT['Forty'].
const NEXT: Record<Exclude<PointCount, 'Forty'>, PointCount> = {
  Love: 'Fifteen',
  Fifteen: 'Thirty',
  Thirty: 'Forty',
};
```

**C#**

```csharp
public enum Player { Player1, Player2 }
public enum PointCount { Love, Fifteen, Thirty, Forty }

// C# n'a pas de type union : la base commune se déclare d'emblée,
// et les quatre états en hériteront.
public abstract class GameState
{
    public abstract string Score { get; }
}

private static PointCount Next(PointCount p) => p switch
{
    PointCount.Love    => PointCount.Fifteen,
    PointCount.Fifteen => PointCount.Thirty,
    PointCount.Thirty  => PointCount.Forty,
    // Depuis Forty on ne progresse pas : on gagne. L'appeler est un bug.
    _ => throw new ArgumentOutOfRangeException(nameof(p)),
};
```

`p1 = 47` n'est plus un bug à attraper : c'est une phrase que le langage ne sait
pas prononcer.

_Points : deux compteurs_

**TypeScript**

```ts
class Points {
  // Constructeur PRIVÉ : personne ne peut fabriquer un Forty-Forty depuis
  // l'extérieur. Cet état-là s'appelle Deuce, et il a sa propre classe.
  private constructor(
    private readonly p1: PointCount,
    private readonly p2: PointCount,
  ) {}

  static start(): Points {
    return new Points('Love', 'Love');
  }

  pointWonBy(scorer: Player): Points | Deuce | Game {
    const mine = scorer === 'player1' ? this.p1 : this.p2;
    const theirs = scorer === 'player1' ? this.p2 : this.p1;

    if (mine === 'Forty') return new Game(scorer);

    const promoted = NEXT[mine];
    if (promoted === 'Forty' && theirs === 'Forty') return new Deuce();

    return scorer === 'player1'
      ? new Points(promoted, this.p2)
      : new Points(this.p1, promoted);
  }

  get score(): string {
    return this.p1 === this.p2 ? `${this.p1}-All` : `${this.p1}-${this.p2}`;
  }
}
```

**C#**

```csharp
public sealed class Points : GameState
{
    private readonly PointCount _p1;
    private readonly PointCount _p2;

    // Constructeur PRIVÉ : personne ne peut fabriquer un Forty-Forty depuis
    // l'extérieur. Cet état-là s'appelle Deuce, et il a sa propre classe.
    private Points(PointCount p1, PointCount p2)
    {
        _p1 = p1;
        _p2 = p2;
    }

    public static Points Start() => new(PointCount.Love, PointCount.Love);

    // GameState et non « Points | Deuce | Game » : C# ne sait pas dire mieux.
    public GameState PointWonBy(Player player)
    {
        var mine   = player == Player.Player1 ? _p1 : _p2;
        var theirs = player == Player.Player1 ? _p2 : _p1;

        if (mine == PointCount.Forty) return new Game(player);

        var promoted = Next(mine);
        if (promoted == PointCount.Forty && theirs == PointCount.Forty) return new Deuce();

        return player == Player.Player1
            ? new Points(promoted, _p2)
            : new Points(_p1, promoted);
    }

    public override string Score => _p1 == _p2 ? $"{_p1}-All" : $"{_p1}-{_p2}";
}
```

_Deuce : aucun champ_

**TypeScript**

```ts
class Deuce {
  // Aucune donnée. Deuce, c'est Deuce.
  // `declare` n'émet aucun JS : ce champ n'existe que pour le compilateur,
  // afin que Deuce ne soit pas imitable par n'importe quel objet de même
  // forme. On y revient dans « Le prix à payer ».
  declare private readonly __brand: never;

  pointWonBy(scorer: Player): Advantage {
    return new Advantage(scorer);
  }

  get score(): string {
    return 'Deuce';
  }
}
```

**C#**

```csharp
public sealed class Deuce : GameState
{
    // Aucune donnée. Deuce, c'est Deuce.

    public Advantage PointWonBy(Player player) => new(player);

    public override string Score => "Deuce";
}
```

Une classe sans champ, dont la seule transition possible retourne un seul type.
Compare avec la version précédente, où le même état traînait quatre champs dont
aucun ne le concernait.

_Advantage : un joueur_

**TypeScript**

```ts
class Advantage {
  constructor(private readonly player: Player) {}

  pointWonBy(scorer: Player): Deuce | Game {
    return scorer === this.player ? new Game(scorer) : new Deuce();
  }

  get score(): string {
    return `Advantage ${this.player}`;
  }
}
```

**C#**

```csharp
public sealed class Advantage(Player player) : GameState
{
    private readonly Player _player = player;

    public GameState PointWonBy(Player scorer)
        => scorer == _player ? new Game(scorer) : new Deuce();

    public override string Score => $"Advantage {_player}";
}
```

_Game : état terminal_

**TypeScript**

```ts
class Game {
  constructor(private readonly winner: Player) {}

  // Pas de pointWonBy. Le jeu est fini. Volontairement.

  get score(): string {
    return `Game ${this.winner}`;
  }
}
```

**C#**

```csharp
public sealed class Game(Player winner) : GameState
{
    private readonly Player _winner = winner;

    // Pas de PointWonBy. Le jeu est fini. Volontairement.

    public override string Score => $"Game {_winner}";
}
```

Relis les deux `score` de la fin. Plus de garde. Plus de `throw`. Plus de test
pour couvrir une branche qui ne peut pas arriver. Le vainqueur est un joueur,
point, parce qu'un `Game` n'existe pas sans vainqueur.

Et les 47 points du début ?

_Le bug devient une erreur de compilation_

**TypeScript**

```ts
const game = new Game('player1');

game.pointWonBy('player1');
//   ~~~~~~~~~~ Property 'pointWonBy' does not exist on type 'Game'.
```

**C#**

```csharp
var game = new Game(Player.Player1);

game.PointWonBy(Player.Player1);
//   ~~~~~~~~~~ CS1061 : 'Game' ne contient pas de définition pour 'PointWonBy'
```

Ce n'est plus une exception à l'exécution, découverte par un utilisateur un
vendredi soir. C'est un **soulignement rouge dans ton éditeur**, avant même
d'avoir sauvegardé.

## La tension : fluidité contre garantie

Puisque chaque transition retourne l'état suivant, on aimerait enchaîner les
appels, et une partie se lirait comme une feuille de match :

_Ce qu'on aimerait écrire_

**TypeScript**

```ts
const game = Points.start()
  .pointWonBy('player2')
  .pointWonBy('player2');
//   ~~~~~~~~~~
// TS2339: Property 'pointWonBy' does not exist on type 'Deuce | Game | Points'.
//         Property 'pointWonBy' does not exist on type 'Game'.
```

**C#**

```csharp
var game = Points.Start()
    .PointWonBy(Player.Player2)
    .PointWonBy(Player.Player2);
//   ~~~~~~~~~~
// CS1061 : 'GameState' ne contient pas de définition pour 'PointWonBy'
```

Ça ne compile pas. Et c'est **logique** : dès le premier point, on ne tient plus
un `Points` mais « l'un des trois états possibles » (dont `Game`, qui n'a
justement pas de méthode).

Cette gêne mérite qu'on s'y arrête, parce qu'elle explique une erreur qu'on voit
souvent. Le réflexe naturel, pour retrouver le chaînage, c'est de donner à l'état
terminal la méthode qui lui manque :

```ts
class Game {
  pointWonBy(): never {
    throw new Error('Le jeu est terminé'); // ⚠️ on vient de tout perdre
  }
}
```

Le chaînage remarche. Les tests redeviennent élégants. Et **la garantie a
disparu** : marquer un point sur un jeu gagné compile de nouveau, et n'échoue
plus qu'à l'exécution. On a réintroduit exactement le bug du premier jet, après
avoir construit quatre classes pour l'éliminer.

L'union est ce qui pousse à cette faute. Manipuler « un état quelconque » exige
que tous les membres partagent la méthode, donc que le terminal l'ait aussi.

La sortie propre, c'est d'aiguiller **une fois**, à un seul endroit :

_Un seul point d'aiguillage_

**TypeScript**

```ts
type GameState = Points | Deuce | Advantage | Game;

function play(points: readonly Player[]): GameState {
  let state: GameState = Points.start();

  for (const player of points) {
    if (state instanceof Game) return state; // le jeu était déjà fini
    state = state.pointWonBy(player);
  }

  return state;
}
```

**C#**

```csharp
public static GameState Play(IEnumerable<Player> points)
{
    GameState state = Points.Start();

    foreach (var player in points)
    {
        // Obligatoire ici : GameState n'expose pas PointWonBy,
        // justement parce que Game ne doit pas l'avoir.
        state = state switch
        {
            Points p    => p.PointWonBy(player),
            Deuce d     => d.PointWonBy(player),
            Advantage a => a.PointWonBy(player),
            Game        => state, // le jeu était déjà fini
            _           => throw new InvalidOperationException(),
        };
    }

    return state;
}
```

Et les tests retrouvent leur lisibilité, sans rien céder :

_Six points partagés, un deuce_

**TypeScript**

```ts
it('atteint Deuce après six points partagés', () => {
  const game = play([
    'player2', 'player2', 'player2',
    'player1', 'player1', 'player1',
  ]);

  expect(game.score).toBe('Deuce');
});
```

**C#**

```csharp
[Fact]
public void Atteint_Deuce_apres_six_points_partages()
{
    var game = Play([
        Player.Player2, Player.Player2, Player.Player2,
        Player.Player1, Player.Player1, Player.Player1,
    ]);

    Assert.Equal("Deuce", game.Score);
}
```

Le compromis est clair : **le typestate coûte la fluidité du chaînage, et paie en
erreurs de compilation.** Si tu tiens absolument au chaînage, tu devras redonner
une méthode à l'état terminal, et renoncer à ce que le pattern t'apportait.

## Ce qui disparaît

C'est le meilleur indicateur qu'un pattern travaille pour toi : compte ce qu'il
supprime.

**La table de transitions disparaît.** Elle n'a plus besoin d'exister : elle est
encodée dans les signatures. `Deuce.pointWonBy(): Advantage` *est* la ligne
`deuce → advantage`. Le diagramme d'états, tu le lis dans les types de retour :

| Signature | Ce qu'elle dit |
|---|---|
| `Points.pointWonBy(): Points \| Deuce \| Game` | trois sorties possibles |
| `Deuce.pointWonBy(): Advantage` | une seule sortie |
| `Advantage.pointWonBy(): Deuce \| Game` | deux sorties |
| `Game`, aucune méthode | état terminal |

**Le champ `state` disparaît.** Il n'y a plus d'état à interroger. Tu ne peux
appeler que ce que le type expose.

**Les champs optionnels disparaissent.** Zéro. Un jeu à Deuce ne transporte plus
quatre champs vides ; il ne transporte rien.

**Les gardes défensives disparaissent**, et avec elles les tests qui les
couvraient, et les branches jamais exercées qui polluaient ton rapport de
couverture.

C'est le principe que Yaron Minsky a résumé en *make illegal states
unrepresentable*. Et c'est exactement le prolongement de
[Tell, Don't Ask](/blog/tell-dont-ask-encapsulation-objet/) : on ne demande plus
son état à l'objet pour décider à sa place, on lui envoie un message et il
retourne ce qu'il est devenu.

## Et ma commande, alors ?

J'ai dit plusieurs fois que ce pattern brille sur les cycles de vie de commande.
Objection légitime : une commande *accumule* des données au fil du processus —
lignes, adresse, paiement, numéro de suivi. Instinctivement, on ajoute des
champs, on ne change pas de type.

Le typestate ne contredit pas cet instinct : les données voyagent avec les
transitions. Ce qui change d'un état à l'autre, ce ne sont pas tant les données
que **les opérations permises**.

_La transition emporte les données_

**TypeScript**

```ts
class DraftOrder {
  private readonly lines: OrderLine[] = [];

  addItem(line: OrderLine): DraftOrder { /* ... */ }
  removeItem(id: LineId): DraftOrder { /* ... */ }

  confirm(shipping: Address): ConfirmedOrder {
    // Les lignes voyagent avec la transition. Rien ne se perd.
    return new ConfirmedOrder(this.lines, shipping, new Date());
  }
}

class ConfirmedOrder {
  constructor(
    private readonly lines: readonly OrderLine[],
    private readonly shipping: Address,
    private readonly confirmedAt: Date,
  ) {}

  // Pas de addItem. Une commande confirmée ne se modifie plus. Volontairement.

  ship(trackingNumber: string): ShippedOrder {
    return new ShippedOrder(this.lines, this.shipping, trackingNumber);
  }
}
```

**C#**

```csharp
public sealed class DraftOrder
{
    private readonly List<OrderLine> _lines = [];

    public DraftOrder AddItem(OrderLine line) { /* ... */ }
    public DraftOrder RemoveItem(LineId id) { /* ... */ }

    public ConfirmedOrder Confirm(Address shipping)
        // Les lignes voyagent avec la transition. Rien ne se perd.
        => new(_lines, shipping, DateTime.UtcNow);
}

public sealed class ConfirmedOrder
{
    private readonly IReadOnlyList<OrderLine> _lines;
    private readonly Address _shipping;
    private readonly DateTime _confirmedAt;

    public ConfirmedOrder(IReadOnlyList<OrderLine> lines, Address shipping, DateTime confirmedAt)
    {
        _lines = lines;
        _shipping = shipping;
        _confirmedAt = confirmedAt;
    }

    // Pas de AddItem. Une commande confirmée ne se modifie plus. Volontairement.

    public ShippedOrder Ship(string trackingNumber)
        => new(_lines, _shipping, trackingNumber);
}
```

`ConfirmedOrder` a *plus* de données que `DraftOrder` : les lignes, plus
l'adresse, plus la date. L'accumulation que tu ferais avec des champs, la
transition la fait en construisant l'état suivant. La différence est ailleurs :
`addItem` a **disparu** de `ConfirmedOrder`. Pas désactivé par un `if`, pas
gardé par une exception — absent de la classe.

Compare avec la version champ statut, que tu as forcément déjà croisée :

_La version champ statut_

**TypeScript**

```ts
addItem(line: OrderLine): void {
  if (this.status !== 'draft') {
    throw new Error('Impossible de modifier une commande confirmée');
  }
  this.lines.push(line);
}
```

**C#**

```csharp
public void AddItem(OrderLine line)
{
    if (_status != OrderStatus.Draft)
        throw new InvalidOperationException("Impossible de modifier une commande confirmée");

    _lines.Add(line);
}
```

Cette garde, il faut l'écrire dans *chaque* méthode de modification, la tester,
et prier pour que le prochain dev qui ajoute `applyDiscount` n'oublie pas la
sienne. Avec une classe par état, la question ne se pose plus : il ne peut pas
appeler une méthode qui n'existe pas, et son IDE le lui dit avant même la revue
de code.

Et le signal des champs optionnels vaut aussi ici : `trackingNumber` n'a de sens
qu'expédiée, `confirmedAt` qu'une fois confirmée. Si ta classe `Order` porte six
champs nullables dont chacun n'est valide que dans certains statuts, ton type
ment — exactement comme le jeu de tennis à Deuce qui trimballait quatre champs
pour rien.

## Le prix à payer

Ce serait malhonnête de s'arrêter là.

**Deuce reste imitable, et c'est structurel.** TypeScript compare les types par
leur **forme**, pas par leur nom. Un objet littéral de même forme est donc
accepté à la place d'une classe, sauf si celle-ci possède un membre privé, qui
la rend nominale.

C'est la vraie raison pour laquelle `p1`, `p2`, `player` et `winner` sont
`private` et pas seulement `readonly` : personne n'en a besoin de l'extérieur, le
`score` suffit. Et l'encapsulation offre la nominalité en prime.

```ts
const p: Points    = { score: 'x', pointWonBy: () => new Deuce() };
// TS2739: missing the following properties from type 'Points': p1, p2
const a: Advantage = { score: 'x', pointWonBy: () => new Deuce() };
// TS2741: Property 'player' is missing
const g: Game      = { score: 'x' };
// TS2741: Property 'winner' is missing

const d: Deuce = { score: 'Deuce', pointWonBy: () => new Advantage('player1') };
// compile. Deuce n'a aucun champ privé à opposer.
```

Trois classes sur quatre sont protégées gratuitement, par les champs privés
qu'elles portaient déjà. `Deuce` n'a rien à cacher, donc rien à opposer. C'est
pour ça, et uniquement pour ça, qu'il porte la ligne suivante :

```ts
declare private readonly __brand: never;
```

Le modificateur `declare` n'émet **aucun JavaScript**. À l'exécution, `Deuce` ne
porte toujours réellement aucune donnée ; le champ n'existe que pour rendre la
classe nominale, et `never` garantit que rien ne pourra jamais lui être affecté.
C'est le prix à payer pour que la promesse tienne aussi au niveau des types :
une ligne, zéro octet.

Attention en revanche au *flavoring*, la variante permissive de cette technique.
Son marqueur est **optionnel** par construction, donc un objet qui ne le porte
pas reste assignable. Il documente l'intention sans la faire respecter. Pour ce
qu'on cherche ici, c'est une marque ou rien.

Et note que le constructeur privé ne remplace pas la marque : il contrôle la
**construction**, pas l'**assignabilité**. Il faut les deux.

C# ne pose aucune de ces questions : ses types sont nominaux par défaut. Cette
ligne est un impôt purement TypeScript.

**Le point d'aiguillage doit exister**, et c'est `play`, le seul de toute
l'implémentation. Il vit à la frontière, là où l'on tient un état dont on ignore
la nature. Tout le reste du code travaille sur des types précis.

C'est aussi l'endroit où les deux langages divergent le plus ; bascule les
onglets de `play` pour le voir. TypeScript compose l'union `Points | Deuce |
Advantage | Game` après coup, sans toucher aux classes, et `pointWonBy` y annonce
précisément ses successeurs possibles. C# exige une classe de base déclarée
d'avance, et son `PointWonBy` ne peut retourner que `GameState` : le compilateur
empêche toujours d'appeler `PointWonBy` sur un `Game`, ce qui est l'essentiel,
mais il ne dit plus *vers quels états* une transition peut mener. Le typestate
fonctionne dans les deux, avec moins de finesse d'un côté.

**Il y a plus de classes.** Quatre types plutôt qu'un, pour un kata de vingt
minutes. Sur un jeu de tennis, c'est discutable. Sur un cycle de vie de commande
avec huit états et des données métier lourdes, ça ne l'est plus
([on l'a vu plus haut](#et-ma-commande-alors-)).

**L'ancien objet reste accessible.** Après `const next = deuce.pointWonBy('player1')`,
la variable `deuce` existe toujours et reste utilisable. Rien n'empêche de
rejouer un point depuis un état périmé.

C'est là que Rust va plus loin. Regarde la signature de la même transition :

```rust
struct Deuce;

impl Deuce {
    // `self` sans `&` : la méthode prend POSSESSION de l'objet.
    // Après l'appel, il n'existe plus.
    fn point_won_by(self, player: Player) -> Advantage {
        Advantage { player }
    }
}

let deuce = Deuce;
let adv = deuce.point_won_by(Player::P1); // `deuce` est consommé ici
let oops = deuce.point_won_by(Player::P2);
//         ^^^^^ error[E0382]: use of moved value: `deuce`
```

Tout tient dans le `self` sans `&`. En Rust, passer une valeur sans référence en
transfère la propriété : la méthode *consomme* l'objet, et la variable d'origine
cesse d'être utilisable. Rejouer un point depuis `deuce` après la transition
n'est pas un bug à attraper en revue de code, c'est une erreur de compilation.
La dernière faille du pattern — l'état périmé qui traîne — se referme.

En TypeScript, en C# ou en Java, rien d'équivalent : le typestate y protège des
opérations invalides, pas de la réutilisation d'un état dépassé. C'est une
limite à connaître, pas une raison de renoncer.

## Alors, lequel choisir ?

<DecisionTable
  rows={[
    {
      when: 'Deux ou trois états, aucune donnée propre, des transitions triviales.',
      then: 'Une énumération',
      note: "N'invente rien.",
    },
    {
      when: 'Beaucoup d\'états, les mêmes données partout, des transitions complexes.',
      then: 'Machine à états',
      note: 'La table centralise les règles et se lit comme un diagramme.',
    },
    {
      when: 'Les états portent des données différentes et exposent des opérations différentes.',
      then: 'Typestate',
      note: 'Une classe par état. Le compilateur devient le gardien.',
      highlight: true,
    },
    {
      when: 'États et données divergents, et un flux de transitions touffu.',
      then: 'Les deux',
      note: 'Typestate pour les données, table pour orchestrer le flux.',
    },
  ]}
/>

Le signal le plus fiable reste celui-ci : **compte tes champs optionnels**. Si
plusieurs d'entre eux sont en réalité obligatoires,
mais seulement dans certains états, ton type ment. Et chaque garde défensive que tu écris pour compenser ce
mensonge est du travail que le compilateur aurait pu faire gratuitement.

Le tennis le dit mieux que n'importe quel diagramme : quand un de tes états n'a
besoin d'**aucune** donnée, et qu'il en transporte quatre, c'est que le modèle
n'est pas au bon endroit.

---

*Tu veux creuser ces sujets avec ton équipe ? J'interviens en
[Brown Bag Lunch](/presentations/) dans vos locaux, ou par mail à
[edouard@lacrafterie.tech](mailto:edouard@lacrafterie.tech).*
