web-dev-qa-db-fra.com

Différence entre les interfaces d'extension et d'intersection dans TypeScript?

Disons que le type suivant est défini:

interface Shape {
  color: string;
}

Maintenant, considérez les moyens suivants pour ajouter des propriétés supplémentaires à ce type:

Extension

interface Square extends Shape {
  sideLength: number;
}

Intersection

type Square = Shape & {
  sideLength: number;
}

Quelle est la différence entre les deux approches?

Et, par souci d'exhaustivité et par curiosité, existe-t-il d'autres façons d'obtenir des résultats comparables?

13
Willem-Aart

Oui, il existe des différences qui peuvent ou non être pertinentes dans votre scénario.

Le plus important est peut-être la différence dans la façon dont les membres avec la même clé de propriété sont traités lorsqu'ils sont présents dans les deux types.

Considérer:

interface NumberToStringConverter {
  convert: (value: number) => string;
}

interface BidirectionalStringNumberConverter extends NumberToStringConverter {
  convert: (value: string) => number;
}

Le extends ci-dessus entraîne une erreur car l'interface de dérivation déclare une propriété avec la même clé que dans l'interface dérivée mais une avec une signature incompatible.

error TS2430: Interface 'BidirectionalStringNumberConverter' incorrectly extends interface 'NumberToStringConverter'.

  Types of property 'convert' are incompatible.
      Type '(value: string) => number' is not assignable to type '(value: number) => string'.
          Types of parameters 'value' and 'value' are incompatible.
              Type 'number' is not assignable to type 'string'.

Cependant, si nous utilisons des types d'intersection

interface NumberToStringConverter = {
    convert: (value: number) => string;
}

type BidirectionalStringNumberConverter = NumberToStringConverter & {
    convert: (value: string) => number;
}

Il n'y a aucune erreur et donné en outre

declare const converter: BidirectionalStringNumberConverter;

converter.convert(0); // `convert`'s call signature comes from `NumberToStringConverter`

converter.convert('a'); // `convert`'s call signature comes from `BidirectionalStringNumberConverter`

// And this is a good thing indeed as a value conforming to the type is easily conceived

const converter: BidirectionalStringNumberConverter = {
  convert: (value: string | number) =>
    typeof value === 'string'
      ? Number(value)
      : String(value)
}

Cela conduit à une autre différence intéressante, les déclarations interface sont ouvertes. De nouveaux membres peuvent être ajoutés n'importe où car les déclarations interface dans le même espace de déclaration et portant le même nom sont fusionnées.

Voici une utilisation courante pour fusionner les comportements

lib.d.ts

interface Array<T> {
    // map, filter, etc.
}

array-flat-map-polyfill.ts

interface Array<T> {
    flatMap<R>(f: (x: T) => R[]): R[];
}

if (typeof Array.prototype.flatMap !== 'function') {
    Array.prototype.flatMap = function (f) {
        return this.map(f).reduce((xs, ys) => [...xs, ...ys], []);
    }
}

Remarquez qu'aucune clause extends n'est présente, bien que spécifiées dans des fichiers séparés, les interfaces se trouvent toutes deux dans la portée globale et sont fusionnées par nom dans une seule déclaration d'interface logique qui possède les deux ensembles de membres. (la même chose peut être faite pour les déclarations de portée de module avec une syntaxe légèrement différente)

En revanche, les types d'intersection, tels qu'ils sont stockés dans une déclaration type, sont fermés, non soumis à la fusion.

Il y a beaucoup, beaucoup de différences. Vous pouvez en savoir plus sur les deux constructions dans le manuel TypeScript. Les sections Interfaces et Types avancés sont particulièrement pertinentes.

15
Aluan Haddad