Difference between extending and intersecting interfaces in TypeScript?
Asked Answered
W

2

123

Let's say the following type is defined:

interface Shape {
  color: string;
}

Now, consider the following ways to add additional properties to this type:

Extension

interface Square extends Shape {
  sideLength: number;
}

Intersection

type Square = Shape & {
  sideLength: number;
}

What is the difference between both approaches?

And, for sake of completeness and out of curiosity, are there other ways to yield comparable results?

Wyrick answered 6/10, 2018 at 16:56 Comment(3)
See Also: Interfaces vs. IntersectionsEllene
@Ellene the docs only states "The principle difference between the two is how conflicts are handled". It doesn't explain how conflicts are handled differently which in the end is not helpful enough. The answer below should be in the docs. github.com/microsoft/TypeScript-Website/blob/… We could open a PR with it :)Mulcahy
One use for intersecting vs extending types: combining two interfaces: type NewInterface = FirstInterface & SecondInterface;Wain
E
136

Yes there are differences which may or may not be relevant in your scenario.

Perhaps the most significant is the difference in how members with the same property key are handled when present in both types.

Consider:

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

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

The extends above results in an error because the derriving interface declares a property with the same key as one in the derived interface but with an incompatible signature.

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'.

However, if we employ intersection types

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

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

There is no error whatsoever and, furthermore, this is useful indeed as a value conforming to this particular intersection type is easily conceived of.

const converter: BidirectionalStringNumberConverter = {
    convert: (value: string | number) => {
        return (
          typeof value === 'string'
            ? Number(value)
            : String(value)
          ) as string & number; // type assertion is an unfortunately necessary hack.
    }
}

Note that the implementation of the intersection, as shown above, involves some awkward types and assertions but these are purely implementation artifacts which do not contribute to the type of the converter object which is solely determined by the type BidirectionalStringNumberConverter used to annotate the object literal converter.

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

const n: number = converter.convert('a'); // `convert`'s call signature comes from `BidirectionalStringNumberConverter`

Playground Link


Another important difference, interface declarations are open ended. New members can be added anywhere because multiple interface declarations with same name in the same declaration space are merged. This is in direct contrast to type expression produced by & which creates an anonymous expression that can be bound to an alias for reuse but not augmented via merging.

Here is a common use for merging behavior

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) { 
    // Implementation simplified for exposition. 
    return this.map(f).reduce((xs, ys) => [...xs, ...ys], []);
  }
}

Notice how no extends clause is present, although specified in separate files the interfaces are both in the global scope and are merged by name into a single logical interface declaration that has both sets of members. (the same can be done for module scoped declarations with slightly different syntax)

By contrast, intersection types, as stored in a type declaration, are closed, not subject to merging.

There are many, many differences. You can read more about both constructs in the TypeScript Handbook. The Object Types and the Creating Types from Types sections are particularly relevant.

Elenaelenchus answered 6/10, 2018 at 17:58 Comment(12)
Great answer. Thanks for pointing out the difference in behaviour when 'overriding' properties, didn't know about that. That alone is a good reason to use types in certain use cases. Can you point out situations where interface merging is useful? Are there valid use cases when building applications (in other words: not libraries)?Wyrick
Willem Aart as you suggest, it is most useful for writing libraries, but what is an application if not a collection of libraries (including your own app). It can be extremely useful for applications as well. Ex: interface Object {hasOwnProperty<T, K extends string>(this: T, key: K): this is {[P in K]?}} which turns Object.prototype.hasOwnProperty into a type guard by introducing an additional, more specific signature for it. .Elenaelenchus
@AluanHaddad the StringToNumberConverter type should be instead named BidirectionalStringNumberConverter, correct? It seems like the other instances were possibly renamed...Lintel
Is this still correct? I plug your examples into the typescript playground and the compiler complains.Jennine
@NathanChappell thank you for catching that. I don't know when that broke. I've updated the example to make it compile, but it now requires a type assertion. I will look into this more.Elenaelenchus
@AluanHaddad thanks. TS seems to be changing quite fast, so it's probably impossible to keep up with it (especially since they seem to have abandoned maintaining a specification...)Jennine
Why is string & number not evaluated as never?Intolerable
@Intolerable because TypeScript doesn't special case such an intersection even though it is indeed impossible at runtime due to the nature of primitives in JavaScript.Elenaelenchus
"This leads to another interesting difference" How are all those statements above this line related to this interesting difference? You provided an example showing "interface can't, but & Type awkwardly can", and then? How is "interface being open-ended" a consequence of it?Batavia
What's the value of this answer? We should not write the non-sense intersection string & number just to make our code work, right? Neither does this contrived example show any practical difference between extends and &.Batavia
@Batavia 1. as string & number is just the type assertion my implementation. I could have used as any. 2. If you think there isn't a practical difference, reread the first 2 paragraphs. The open-endedness has to do with the fact that & results in a type which can be assigned to an alias. Aliases aren't don't allow declaration merging. I agree with you that the my segway is misleading. I'll improve it. Obviously plenty have found value here but I'm always happy to improveElenaelenchus
@AluanHaddad OK, thanks for your clarification. I will read your update carefully then and maybe provide some feedback.Batavia
N
0

Typescript has updated their documentation with a section describing this scenario!

https://www.typescriptlang.org/docs/handbook/2/objects.html#interfaces-vs-intersections

"The principle difference between the two is how conflicts are handled, and that difference is typically one of the main reasons why you’d pick one over the other between an interface and a type alias of an intersection type."

Nightclub answered 15/9, 2022 at 21:39 Comment(1)
This literally says nothing. I found it in the Handbook and needed to google the answer, because the Handbook is so vague about it.Quarta

© 2022 - 2024 — McMap. All rights reserved.