Typage structurel vs nominal
Comment un langage décide si deux types sont compatibles : par leur forme (structurel, comme TypeScript) ou par leur nom déclaré (nominal, comme C# ou Java). Une différence subtile aux grosses conséquences.
Typage structurel vs nominal
Une fois qu'on sait quand un langage vérifie les types, reste une autre question, plus subtile : comment décide-t-il que deux types sont compatibles ? Deux écoles s'opposent - et c'est une vraie surprise quand on passe de C# à TypeScript.
# La question : « ce type en accepte-t-il un autre ? »
Quand tu passes un objet à une fonction qui attend un certain type, le langage doit décider : est-ce compatible ? Il y a deux façons radicalement différentes de répondre.
- Nominal : compatible si les types portent le même nom ou ont une relation déclarée (héritage, implémentation d'interface).
- Structurel : compatible s'ils ont la même forme (les mêmes champs, les mêmes méthodes) - peu importe leur nom.
# Typage nominal (C#, Java)
Ce qui compte, c'est l'identité déclarée du type. Deux types aux champs identiques restent incompatibles tant qu'on n'a pas explicitement déclaré un lien entre eux.
class Point2D { public int X; public int Y; }
class Vecteur2D { public int X; public int Y; }
Point2D p = new Vecteur2D(); // ❌ Erreur : ce sont deux types distincts,
// malgré des champs identiquesclass Point2D { public int X; public int Y; }class Vecteur2D { public int X; public int Y; } Point2D p = new Vecteur2D(); // ❌ Erreur : ce sont deux types distincts, // malgré des champs identiquesPour qu'un Vecteur2D soit accepté là où on attend un Point2D, il faudrait qu'il hérite de Point2D ou implémente une interface commune. Le nom et la filiation font foi.
# Typage structurel (TypeScript, Go)
Ce qui compte, c'est la forme. Si un objet a tout ce que le type attend, il est accepté - même s'il n'a jamais « déclaré » quoi que ce soit.
interface Point {
x: number;
y: number;
}
function afficher(p: Point) {
console.log(p.x, p.y);
}
const v = { x: 10, y: 20, z: 5 }; // un simple objet, aucune déclaration
afficher(v); // ✅ accepté : il a bien x et y, la forme correspondinterface Point { x: number; y: number;} function afficher(p: Point) { console.log(p.x, p.y);} const v = { x: 10, y: 20, z: 5 }; // un simple objet, aucune déclarationafficher(v); // ✅ accepté : il a bien x et y, la forme correspondv n'implémente rien du tout, mais il a la bonne forme : TypeScript le considère comme un Point valide. C'est ce qu'on résume par « si ça a la bonne forme, ça passe ».
# Les conséquences
| Nominal (C#, Java) | Structurel (TS, Go) | |
|---|---|---|
| Compatibilité | Par nom / relation déclarée | Par forme (les membres) |
| Souplesse | Stricte : il faut déclarer les liens | Grande : pas de boilerplate |
| Risque | Verbeux | Compatibilités « accidentelles » |
Le structurel évite beaucoup de cérémonie (pas besoin de déclarer « j'implémente telle interface »), au prix de compatibilités parfois involontaires. Le nominal est plus strict : deux choses ne sont interchangeables que si tu l'as explicitement voulu.
# À retenir
- La question : comment décide-t-on que deux types sont compatibles ?
- Nominal (C#, Java) : par le nom et les relations déclarées (héritage, interfaces).
- Structurel (TypeScript, Go) : par la forme - mêmes membres = compatibles.
- Le structurel est plus souple mais peut créer des compatibilités accidentelles.
À lire ensuite : Le duck typing, le cousin dynamique du typage structurel.