TypeScript - 2/3 : Pourquoi l'adopter

typescript-logo

Suite de la première partie du dossier.

Pourquoi l'adopter ?

Les articles présentant les avantages et inconvénients de TypeScript ne manquent pas sur la toile. J'aborde ici une liste non-exhaustive des points forts qui justifient que l'on s'intéresse au langage.

Deux citations d'Anders Hejlsberg au sujet de JavaScript, qui, au risque de sonner comme un troll, auront certainement le mérite de faire sourire les développeurs JavaScript expérimentés :

Erik Meijer: Vous insinuez que l'on ne peut pas écrire de grosses applications en JavaScript ?
Anders Hejlsberg: Non, vous pouvez les écrire. Mais vous ne pouvez pas les maintenir.
Lang.NEXT 2012

Et plus récemment :

Les très gros codes source JavaScript ont tendance à finir en "Lecture seule".
Anders Hejlsberg, Microsoft Build 2016


Avantages du Typage

Le typage, bien qu'optionnel, apporte de nombreux avantages, dont :

  • L'analyse statique (sans exécution) du code
  • L'amélioration significative de la capacité à maintenir et réusiner (refactor) le code
  • Un code plus lisible et plus rapidement compréhensible (les annotations de type jouant aussi un rôle de documentation)

Analyse statique

TypeScript est accompagné de fichiers de déclaration des API d'ES5, ES6, du DOM, etc. La plupart des packages (npm) majeurs contiennent aussi des fichiers de déclaration (sinon, on peut souvent les trouver dans les packages tiers, comme @types/express).
tsc est capable d'en tirer parti pour anticiper des erreurs éventuelles de programmation, au-delà de la simple analyse (élémentaire) inhérente aux règles du langage (comme, par exemple, l'impossibilité de multiplier deux string, ou de passer une string en argument d'une fonction attendant un number).

import * as diacritics from 'diacritics';

function normalizeName(name: string) {  
  return diacritics.remove(name.toLocalUpperCase());
}

normalizeName(undefined);  

tsc (ou votre IDE, indirectement) signale une erreur à la ligne 4 :

error TS2551: Property 'toLocalUpperCase' does not exist on type 'string'. Did you mean 'toLocaleUpperCase'?

Après correction, tsc signale une autre erreur, ligne 7 (seulement si le mode strict est activé) :

error TS2345: Argument of type 'undefined' is not assignable to parameter of type 'string'

Ce qui marche avec les types basiques, ici, marche avec tous les types :

import * as _ from 'lodash';

class Person {  
  constructor(readonly firstName: string, readonly lastName: string) {}
}

const mike = new Person('Mike', 'Smith');  
const partialMike = _.pick<Person, keyof Person>(mike, 'firstName', 'age');  

VSCode signale une erreur ligne 8, sur 'age' :

error TS2345: Argument of type '"age"' is not assignable to parameter of type 'Many<"firstName" | "lastName">'.

La définition de pick dans @types/lodash spécifie que seules les propriétés listées dans le second paramètre de type (keyof Person) sont acceptées (présentation de keyof). Ce morceau de code est donc "robuste" à toute évolution de Person (propriété renommée, supprimée, etc.) et évite aussi certaines étourderies comme les fautes de frappe dans les noms de propriétés/méthodes..

Refactoring

Le typage permet aux IDE de mieux nous servir. L'autocompletion, la propagation du renommage de méthodes, propriétés, variables, la fonctionnalité "Find usages" (ou "Find references")... Ces outils fonctionnent souvent bien mieux une fois la codebase typée.

Aussi, bien que cela soit de moins en moins marqué grâce à l'excellent support d'ES6 par les IDE, le refactoring prend une autre ampleur grâce à l'analyse statique : on peut renommer une propriété, une méthode ou une variable, sans craindre d'induire des erreurs de runtime comme TypeError: cannot read property 'x' of undefined, ou encore TypeError: x.y() is not a function. En effet, la transpilation échoue si l'on tente d'accéder à une propriété/méthode/variable non définie.

refactor-man


Les nouveautés d'ECMAScript

À l'heure où cet article est écrit, TypeScript 2.8 est vieux de seulement quelques semaines. Pourtant, on ne compte plus les proposals du comité de normalisation d'ECMAScript (TC39) ayant été intégrées assez tôt dans le langage :

On est aussi en mesure d'utiliser la plupart de ces fonctionnalités (et plus encore) avec Babel. Cependant, il est impossible pour le moment (Babel 6+) d'intégrer du code TypeScript à votre chaîne de transpilation Babel. Il faudra donc attendre que Babel 7.0 soit publiée !  

You can now use babel-preset-typescript to allow Babel to strip types similar to how babel-preset-flow works!
[...] you setup TS with --no-emit or use it in the editor/watch mode so that you can use preset-env and other Babel plugins.


Programmation orientée Objet

Les concepts de la POO listés ci-après n'ont pas vocation à être présentés dans cet article. Si certains vous sont inconnus, découvrez-les dans la documentation officielle

Interface

Concept bien connu de la programmation orientée objet que l'on retrouve dans TypeScript. Les interfaces n'existent que dans le code TypeScript : il n'en reste rien dans la version transpilée en JavaScript. Cependant, elles ont de multiples avantages :

  • Documenter le code en spécifiant la structure attendues des données passées en paramètres ou retournées par les fonctions.
  • Permettre la validation des déclarations de classes qui les implémentent (rôle de Contrat).
  • Étendre les interfaces de modules existants grâce à la Combinaison de déclaration ou Declaration merging (concept propre à TypeScript dont je recommande la lecture)

Class

L'arrivée d'ECMAScript 2015 a apporté le support des classes en JavaScript. Cependant, certaines notions de la POO sont manquantes. C'est le cas notamment :

  • Des classes et méthodes abstraites
  • Des attributs de visibilité (ou accessibilité) private, protected, public

Que l'on retrouve dans TypeScript.

Mais aussi...

Les Énumérations et les Génériques (ou Types paramétrés / Templates). Bien que n'étant pas des concepts propres à la POO, ils existent dans la plupart des grands langages orientés object.

Un mot sur le generic programming

Le support des Génériques (et TypeScript 2.8, avec ses types conditionnels) permet de définir des types avancés comme :

export type StringProps<T> = ({ [P in keyof T]: T[P] extends string ? P : never })[keyof T];  

regroupant toutes les propriétés de type string de T.

Exemple d'utilisation :

class Person {  
  constructor(
    public readonly firstName: string,
    public readonly email: string,
    public readonly age: number,
    public readonly isMarried: boolean,
  ) {}
}

function serialize<T>(obj: T, fields: Array<StringProps<T>>) {  
  // ...
}

Ici, si l'on passe un objet Person (notre paramètre de type T) à la fonction serialize, le paramètre fields n'acceptera qu'un tableau de ses propriétés de type string.

const mike = new Person('Mike', 'Smith', 25, false);

// ✓
serialize(mike, ['firstName']);  
serialize(mike, ['firstName', 'email']);

// error TS2345: Type '"firstname"' is not assignable to type '"firstName" | "email"'
serialize(mike, ['firstname']);  
// error TS2345: Type '"age"' is not assignable to type '"firstName" | "email"'
serialize(mike, ['firstName', 'age']);  


Support de JSX

TypeScript supporte JSX intégralement (typage, analyse et transpilation). Pour l'exploiter, les fichiers contenant du JSX doivent avoir l'extension .tsx et l'option de compilation jsx (tsconfig.json > compilerOptions) doit être activée. Trois valeurs sont disponibles pour cette dernière :

  • preserve : Le code JSX n'est pas transpilé et le fichier de sortie a pour extension .jsx - utile si vous souhaitez confier le traitement de JSX à un autre outil (Babel, par exemple)
  • react : Le code JSX est transpilé vers du code React (React.createElement) et le fichier de sortie a pour extension .js
  • react-native : comme preserve, conserve le code JSX dans l'état, mais le fichier de sortie a pour extension .js

Prenons le composant React suivant, représentant une cellule cliquable sur un plateau de jeu :

export default function Cell(props: {player?: PlayerEnum}) {  
  const baseClass = 'c4__cell';
  const cellClasses = `${baseClass} ${classNames({
    [`${baseClass}--empty`]: (props.player === PlayerEnum.NONE),
    [`${baseClass}--a`]: (props.player === PlayerEnum.A),
    [`${baseClass}--b`]: (props.player === PlayerEnum.B),
  })}`;

  return <button className={cellClasses} />;
}

Le composant Cell accepte une prop player, dont la valeur peut être undefined ou bien compatible avec l'énumération PlayerEnum :

export enum PlayerEnum {  
  NONE = 0,
  A = 1,
  B = 2,
}

La ligne suivante provoquera une erreur de compilation (un IDE compatible vous signalera l'erreur en amont) :

<Cell key={`c-${x}-${y}`} player={'A'} />  

Types of property 'player' are incompatible. Type 'string' is not assignable to type 'PlayerEnum | undefined'.

Grâce à ce typage, VS Code sait nous dire quel type de propriété est attendue :

ts-jsx-intellisense-1

Et bien évidemment, avec l'autocompletion :
ts-jsx-intellisense-2

Le code JSX de notre composant Cell (return <button...) est transpilé ainsi :

return React.createElement("button", { className: cellClasses });  

On utilise JSX.Element pour typer des éléments JSX (par exemple, retournés par une fonction). Le namespace JSX est défini dans le package @types/react, qui contient (entre autres) la définition de tous les éléments HTML adaptée au JSX :

interface IntrinsicElements {  
  // HTML
  a: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
  abbr: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
  address: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
  area: React.DetailedHTMLProps<React.AreaHTMLAttributes<HTMLAreaElement>, HTMLAreaElement>;
  article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
  // ...

La liste n'est évidemment pas exhaustive et les arguments en faveur de TypeScript (pour le frontend, comme pour le backend) ne manquent pas.

Installation et Migration

À suivre dans la troisième partie du dossier.