Types vs. interfaces

1/3)

Introduction

En fait, en TypeScript, un type et une interface c'est à peu de choses près la même chose.

La différence principale vient de la façon dont on peut combiner ces deux "types de types":

Une interface peut étendre une autre interface, tandis qu'un type ne peut ni étendre un autre type ni étendre une interface.

Les types peuvent se combiner les uns avec les autres, soit par union, soit par intersection.

Cette distinction de comportement entre types et interface reflète bien les fameux paradigmes de l'extension de fonctionnalité - qu' on oppose souvent - que sont la composition et l' héritage. TypeScript propose des constructions pour chacun des deux clans.

2/3)

Une interface peut en étendre une autre

Les interfaces en TypeScript sont, je pense, similaires aux interfaces dans tous les langages qui ont des interfaces : elles décrivent la "forme" que peut prendre un objet, une classe, une fonction... mais elle n'en définissent pas l'implémentation.

Une interface peut étendre une autre interface, en voici un exemple :

"TextNode" étend "Node" :
interface Node {
  type: string
}

interface TextNode
  extends Node {
    type: 'TextNode'
    value: string
  }
Zoomez ou dé-zoomez l'exemple avec la commande ci-dessous :

Cela dit, la notion d'extension ou d'implémentation enTypeScript  est un peu différente d'autres langages comme le C++ ou le PHP par exemple.

En effet on pourrait dire qu'en TypeScript  tout étend à peu près tout, dans la mesure où même si on type les paramètres d'une fonction, on pourra lui passer n'importe quel type jugé compatible  par TypeScript.

Exemple d'usage d'un mauvais type, mais compatible :
type Rectangle = {
  width: number
  height: number
}

type Square = {
  width: number
  height: number
}

const computeArea = (rect: Rectangle) =>
  rect.width * rect.height;

const square1: Square = {
  width: 10,
  height: 10,
};

console.log(computeArea(square1));
Zoomez ou dé-zoomez l'exemple avec la commande ci-dessous :

En PHP si vous déclarez que votre fonction a besoin d'un paramètre implémentant telle interface, vous ne pourrez utiliser cette fonction qu'avec un paramètre implémentant explicitement l'interface attendue.

En TypeScript, en gros, tant que l'analyse statique du code conclut que ça va marcher, vous faites ce que vous voulez.

3/3)

Union et intersection de types

Les types en TypeScript peuvent se combiner par union avec l'opérateur "|", ou par intersection avec l'opérateur "&", pour créer des types plus complexes.

Exemples d'intersection et d'union de types :
type Person = {
  name: string
}

type Traveller = Person & {
  hasPassport: true
}

type Robot = {
  CPUs: number
  RAM: number
  version: string
}

type Employee = Person | Robot
Zoomez ou dé-zoomez l'exemple avec la commande ci-dessous :

Quand on fait une intersection de deux types, on déclare que le type résultant aura chacune des propriétés des deux types, en même temps.

Quand on fait une union de deux types, on déclare que le type résultant aura un sous-ensemble de l'union des propriétés des deux types.Mais pas n'importe quel sous-ensemble, un objet, pour appartenir à une union, doit avoir au moins toutes les propriétés de l'un des types de l'union. On verra ça dans l'exemple d'après.

Union de types :
interface Rectangle {
  width: number
  height: number
}

const computeArea = (rect: Rectangle) =>
  rect.width * rect.height;

type hasWidth = {
  width: number
}

type hasHeight = {
  height: number
}

type UnionSquare = hasWidth | hasHeight;

const unionSquareW: UnionSquare = {
  width: 15,
};

const unionSquareH: UnionSquare = {
  height: 15,
};

const unionSquare: UnionSquare = {
  width: 15,
  height: 32,
};

/**
 * La ligne qui suit provoque l'erreur suivante :
 *
 * Argument of type 'UnionSquare' is not assignable to
 * parameter of type 'Rectangle'. Property 'height'
 * is missing in type 'hasWidth' but
 * required in type 'Rectangle'.
 */

console.log(
  computeArea(unionSquare),
);

type InterSquare = hasWidth & hasHeight

const interSquare: InterSquare = {
  width: 30,
  height: 30,
};

/**
 * Mais ça, ça va très bien :
 */

console.log(
  computeArea(interSquare),
);
Zoomez ou dé-zoomez l'exemple avec la commande ci-dessous :

Comme je le disais plus haut: pour être mêmbre d'une union de types A et B, on objet peut avoir n'importe laquelle des propriétés de A ou de B, mais il doit au moins avoir soit toutes les propriétés de A, soit toutes les propriétés de B.

Un membre d'une union doit correspondre à au moins un type :
type hasWidth = {
  width: number
}

type hasHeight = {
  height: number
  unitOfMeasurement: string
}

type UnionSquare = hasWidth | hasHeight;

const unionSquareW: UnionSquare = {
  width: 15,
};

/**
 * La ligne qui suit provoque l'erreur suivante :
 *
 * Type '{ height: number; }' is not assignable to type
 * 'UnionSquare'. Property 'unitOfMeasurement'
 * is missing in type '{ height: number; }'
 * but required in type 'hasHeight'.
 */

const unionSquareH: UnionSquare = {
  height: 15,
};
Zoomez ou dé-zoomez l'exemple avec la commande ci-dessous :

Enfin, ça va de soi, mais un membre d'une union de types ne peut pas avoir de propriétés qui ne sont dans aucun des types de l'union.

Les propriétés doivent être définies sur au moins un type :
type hasWidth = {
  width: number
}

type hasHeight = {
  height: number
}

type UnionSquare = hasWidth | hasHeight;

/**
 * La ligne qui suit provoque l'erreur suivante :
 *
 * Type '{ width: number; somethingElse: boolean; }'
 * is not assignable to type 'UnionSquare'.
 * Object literal may only specify known properties,
 * and 'somethingElse'
 * does not exist in type 'UnionSquare'.
 */

const union: UnionSquare = {
  width: 15,
  somethingElse: true;
};
Zoomez ou dé-zoomez l'exemple avec la commande ci-dessous :
© François-Marie de Jouvencel
fm.de.jouvencel@gmail.com