I keep seeing the type vs interface in TypeScript keep coming up, and people citing various performance reasons to keep using interface. I wanted to point out WHY those of using are choosing intentionally to use type over interface knowing the performance costs.
Those of using type aren’t using it to make our compiler slow. We’re using it to model our data using simple Ands and Ors, make impossible situations impossible using Unions & Records, and keep the types and compiler errors simple.
// And
type Dog = { name: string, age: number, breed: Breed }
// Or
type Breed = 'Sheltie' | 'Lab' | 'Mutt'
People who use Interface are typically Object Oriented friendly, and are ok using Extends, and merging various Interfaces.
That’s not what those of us using Types are doing. We define the thing or states of the thing; and that’s it. A lot of the OOP devs will favor composition over inheritance, but still use multiple extends because of their integration, usually with a framework like React or even just HTML where inheritance still is everywhere. I’m not saying they do this intentionally, but interfaces that extend interfaces hide things, especially where you can add things later in TypeScript.
Using the type keyword, we’re not hiding anything. In fact, if you add things later, the compiler yells at you. Good, don’t want 2 sources of truth. There are 2 places you can, though, hide things.
Intersection types aren’t what us Functional Programmers use. There are exceptions to every rule, so I know there is someone reading this who is like “Well, actually…”. That sounds great, but that’s not us FP devs are doing. We’re defining 1 thing. If another thing is different; it gets a new type. The OOP fans will say interface can have new fields added to the type after that fact as a feature. We FP devs see that as a bug, not a feature, and appreciate the compiler ensuring “There is only 1 Thing”. You can’t screw it up elsewhere because there is only 1 source of truth.
Now, caveat. If you do see an intersection type with &, in an FP code base, it has NOTHING TO DO WITH INHERITANCE. “Yes, but they’re treated as the same type”, well no; we’re putting those types into Unions usually. We use the intersection type character to save how much we have to copy paste, and NEVER treat it as the same type because it’s used in a union which allows only 1 type at a time.
For example, in Union types, many languages allow you to go:
// Example code
type Polygon
= Point { x: number, y: number }
| Vector { x: number, y: number, z: number }
| Rectangle { x:number, y:number, width: number, height: number }
You can’t do that in TypeScript without extra work. So let’s do the work so the compiler can help us.
// Real TypeScript
type Point = { type: 'point', x: number, y: number }
type Vector = { type: 'vector', x: number, y: number, z: number }
type Rectangle = { type: 'rectangle', x:number, y:number, width: number, height: number }
Now that the TypeScript types are defined, we can union them:
type Polygon = Point | Vector | Rectangle
Now a Polygon is ONE OF the three; it’s either a Point, OR it’s a Vector, OR it’s a Rectangle.
We can make a simple string to print it with TypeScript’s help. We’ll make a Vector:
const getVector = (x:number, y:number, z:number):Vector => {
return {x, y, z, type: 'vector' }
}
Then make a polygon logger:
const whatIsIt = (thing:Polygon):string => {
switch(thing.type) {
case 'point':
return 'On point'
case 'vector':
return "What's the vector, Victor?"
case 'rectangle':
return 'A rectangle'
}
}
Then actually run it:
console.log(whatIsIt(getVector(1, 2, 3))) // What's the vector, Victor?
Now, your DRY alarm bells may be ringing. Me too! Let’s use an intersection on those types to reduce the typing:
type XandY = { x: number, y: number }
type Point = { type: 'point' } & XandY
type Vector = { type: 'vector', z: number } & XandY
type Rectangle = { type: 'rectangle', width: number, height: number } & XandY
Note, if you’re from an OOP background, this might look immediately familiar. However, notice our logging code doesn’t change, still logs out Vector, and we’re not doing any “as” or “instanceof” weak type shenanigans.
Again, Polygons are still one of the 3; not somehow borrowing other properties. The only reason we did the intersection symbol & was to save some typing. The above is just 2 small properties, but once you start modelling your domain with types, they get long, and quick. In ReScript or Elm, we’d copy pasta. Well… it’s worse in ReScript, actually, we’d start changing names because you can’t have record types that similiar.
When people make the recommendation to use Interface for speed reasons, fine, makes sense. However, if it’s said “it’s recommend” wholesale, no, no it’s not. That’s not true.
Interface is recommended for Object Oriented Developers, yes; interface melds nicely with classes, and inheritance, and multi-inheritance, and design patterns, etc. There is another recommendation, however. It’s one of the reasons they added Type in the first place; they didn’t add Type after Interface, and intentionally make it slower.
If you’re doing Functional Programming, you can leverage the simple power of And’s and Or’s using the type keyword in TypeScript which provides stricter types, yet still interfaces with a lot of the shortcuts TypeScript provides to reduce how much you have to type in other languages.
I recommend using the Type keyword knowing the TypeScript compiler performance overhead; the trade off is stricter types and more readable compiler messages (which are still pretty bad). If I had choice, sure, I’d use ReScript or OCaml which do not have these performance issues TypeScript has, amongst other benefits, but sometimes you don’t have a choice; the team or job requires TypeScript so you make the best with what you got.
Top comments (0)