Soyez plus exhaustifs dans vos types (avec TypeScript)

Imaginez que vous avez un besoin de différencier deux types qu'on appellera Roman et Arabic pour représenter deux notations de nombres.

Dans Roman, une méthode text() nous donnera du texte :

class Roman {
  text(): string {
    return 'I';
  }
}

Dans Arabic, une méthode number() nous donnera un nombre :

class Arabic {
  number(): number {
    return 2;
  }
}

Pour assembler ces deux types, je vous propose d'utiliser les Union Types :

type Numeral = Roman | Arabic;

Maintenant, on a soit Roman soit Arabic pour le type Numeral, c'est exhaustif.

Ça permet de faire des choses comme :

const stringify = (numeral: Numeral): string => {
  if (numeral instanceof Roman) {
    return numeral.text(); // Ça ne peut être qu'un Roman
  }
  return numeral.number().toString(); // Il ne reste plus que Arabic vu qu'on a déjà traité le Roman
};

Ici stringify donne le résultat des méthodes spécifiques à Roman et Arabic sous forme de texte.

Parfois, on se dit que ça serait bien d'extraire le instanceof dans une méthode :

abstract class NumeralIs {
  isRoman(): boolean {
    return this instanceof Roman;
  }
}

class Roman extends NumeralIs {/* … */}
class Arabic extends NumeralIs {/* … */}

Sauf que quand on fait ça, on perd l'inférence de type :

const stringify = (numeral: Numeral): string => {
  if (numeral.isRoman()) {
    return numeral.text(); // C'est cassé parce que le type est toujours Numeric
  }
  return numeral.number().toString(); // C'est cassé pour la même raison
};

Pour que tout fonctionne à nouveau, il existe le mot clé is en TypeScript qui permet d'assurer le type de retour suite à une opération booléenne :

abstract class NumeralIs {
  isRoman(): this is Roman { /* ... */ } // On affirme que si c'est vrai, ça sera un Roman
}

Maintenant l'inférence fonctionne à nouveau :

const stringify = (numeral: Numeral): string => {
  if (numeral.isRoman()) {
    return numeral.text(); // Roman est bien retrouvé
  }
  return numeral.number().toString(); // Roman a été traité, ça ne peut plus être que Arabic
};

Pour résumer, on vient de voir les concepts théoriques suivants :

Et aussi, le is de TypeScript qui est peu connu.

N'hésitez pas à pratiquer le Type-Driven Development pour poursuivre vos connaissances sur les types et leurs usages.