Le "Type Narrowing"

1/3)

Une fonctionnalité épatante

C'est vraiment la fonctionnalité de TypeScript qui m'émerveille le plus pour le moment.

Le "type-narrowing" - que je traduis par "découverte incrémentale des types" (mais je n'ai trouvé aucune traduction du terme nulle-part et donc j'ai du l' inventer, ce n'est peut-être pas la plus juste... cela-dit, personne ne parle de TypeScript en français donc je fais ce que je veux !) mais je crois que dans la suite je dirai plus simplement type-narrowing ou encore simplement narrowing - est en gros :

le fait, pour le compilateur, d'assigner à un symbole (une variable, une fonction) ou même à une expression en général un type, d'abord assez générique, puis, selon l'usage de l'expression fait par le code, de déduire un type plus précis, de resserrer ("narrow" = étroit) le champ des possibles pour le type considéré.

2/3)

La philosophie du système de types en TypeScript

Pour moi le narrowing est la clef de voûte de TypeScript.

J'ai déjà utilisé plein de langages typés (TurboPascal, C, C++, Haskell...) et j'ai toujours trouvé que les types étaient d'un grand intérêt pour minimiser les erreurs d'exécution. Mais je n'ai jamais vu un système de types se comporter comme celui de TypeScript.

En même-temps, pour rajouter des types à un langage par nature aussi anarchique que JavaScript sans pourrir complètement l'expérience de développement il fallait frapper fort.

Et c'est exactement ce qu'à fait l'équipe de TypeScript. Bravo.

J'avais longtemps été réticent à utiliser TypeScript parce que j'étais persuadé que cela allait dénaturer  JavaScript, me rendre moins productif.

Ça c'était à cause des système de types que je connaissais : tous très rigides.

Mais le système de types de TypeScript est à ma connaissance unique en son genre.
C'était vraiment la manière intelligente de typer JavaScript.

En gros, le système de types de TypeScript pourrait se résumer à cette question, qui est celle que je m'imagine que le compilateur se pose en permanence :

"Est-ce que, en fonction de tout ce que je sais à ce stade, l'utilisateur est en train de potentiellement faire une grosse connerie ou pas ?"

Et la grande finesse de TypeScript tient dans le :
tout ce que je sais à ce stade

C'est à dire que TypeScript ne va pas se contenter de regarder les types des symboles qu'on manipule.
Il va aussi regarder comment on les manipules, et intégrer dynamiquement  dans la résolution des types les informations sûres qu'il est capable de déduire du code qu'on écrit.

Souvent ce qui se passe, c'est qu'on va passer d'un type assez vague à un type très précis, et ce de manière sûre, c'est pourquoi je me concentre autant sur le narrowing dans ce chapître.

3/3)

Une Comparaison au C++ qui Fait Briller TypeScript

Souvent, en C++, je me retrouvais à vouloir faire des choses du genre de ce qui suit :

Quelque chose qui ne marche pas en CPP :
class Base {
  public:
  std::string type;
};

class A : public Base {
  public:

  A() {
    type = 'A';
  }
};

class B : public Base {
  public:
  std::string somethingOnlyBHas;

  B() {
    type = 'B';
    somethingOnlyBHas = "this is my secret message";
  }
};

void useInstance(Base *inst) {
  std::cout << "Instance type " << inst->type << std::endl;

  if (inst->type == "B") {
    // erreur : class "Base" has no member "somethingOnlyBHas"
    std::cout << inst->somethingOnlyBHas << std::endl;
  }
}
Zoomez ou dé-zoomez l'exemple avec la commande ci-dessous :

Il y a bien sûr une solution, mais le problème est qu'elle nous fait perdre toute l'aide du compilateur.

La solution est de brutalement caster Base vers B, ce qu'on peut faire, mais qui nous fait perdre tout contrôle du compilateur car le compilateur ne cherche pas à savoir si inst peut être castée en B.

Le contournement qui marche en CPP :
void useInstance(Base *inst) {
  std::cout << "Instance type " << inst->type << std::endl;

  if (inst->type == "B") {

    // Ici, le compilateur se fiche pas mal que
    // inst soit vraiment de type B, si jamais il y a d'autres
    // classes qui héritent de Base et qui ont le type B,
    // on court droit à la segfault
    B *b = (B *) inst;

    std::cout << b->somethingOnlyBHas << std::endl;
  }
}
Zoomez ou dé-zoomez l'exemple avec la commande ci-dessous :

Voilà un équivalent qui marche et qui est sûr en TypeScript :

Exemple de narrowing dans un cas d'héritage :
interface Base {
  type: string
}
interface A extends Base {
  type: 'A'
}

interface B extends Base {
  type: 'B'
  somethingOnlyBHas: string
}

type ChildOfBase = A | B;

const useInstance = (inst: ChildOfBase) => {
  if (inst.type === 'B') {
    console.log(inst.somethingOnlyBHas);
  }
};
Zoomez ou dé-zoomez l'exemple avec la commande ci-dessous :
© François-Marie de Jouvencel
fm.de.jouvencel@gmail.com