Explore TypeScript's nuances - "type" vs. "interface." Uncover use cases and their impact on compile and build times for a comprehensive understanding.
Why types vs interfaces?
To be able to use TypeScript features fully and with pride, today we'll explore these concepts:
- differences in syntax, use cases and mindset,
- mapped types don't work with interfaces,
- unions don't work with interfaces,
- differences in declarations merging,
- types can be merged with interfaces and vice-versa,
- types and interfaces with classes,
- cosmetic differences,
- impact on build/compile time.
At the end, we'll know the answer to the question: "Should I use types or interfaces?"
Differences in syntax, use cases and mindset
In TypeScript we can create type definitions (contracts for our code) in following ways:
However, the interface keyword is reserved only for objects creation, but with type we can create definitions for everything.
Mapped types don't work with interfaces
Mapped types allows you to create new types by transforming the properties of an existing type based on a mapping function. They are often used to perform operations on the keys and/or values of an object type to create a new type.
Unions don't work with interfaces
If you want to create a declaration of an object that is A or B with interfaces, it's not possible.
Differences in declarations merging
Interfaces implements something that is called automatic declaration merging. How it works? You need to define more than one interface with the same name and their definitions will be merged.
The same attempt done in type context doesn't work - they must have unique names per scope.
So, we know that interfaces have additional feature - implict declarations merging. In another way, declarations with type keyword must have unique names per scope.
However, we can merge declarations in explicit way and the syntax in both is different.
The result will be the same object. The only difference between them in this context is the syntax. For interfaces we need to use extends keyword but for type we need to use merge operator - "&".
But why did someone added auto-merge for interfaces? Imagine a library with badly written type definitions and you want to consume this library API. There is an additional function added, but someone forgot to add a type definition for this function or it's done in the wrong way. Thanks to this feature you are able to override it easily.
The other use case will be just removing the need to write the extends keyword all the time. It's the next, fancy feature. We have a lot of them in JS. Instead of typing we just need to know how something works.
The real-world example will be a situation in which library doesn't have any type definitions. In the example below, we're adding full typing for gatsby-plugin-dark-mode library.
Types can be merged with interfaces and vice-versa
Type and interface declarations are compatible in the merging aspect.
Types and interfaces with classes
Both may be used in the context of classes in the same way - to implement contracts.
Look at the following code sample to understand the amount of code that we need to write for both.
Type-based declarations are just easier to write :P, that's why a lot of current eslint rules detect such cases as above and warn you to rewrite it to type syntax.
The difference in developer experience
Let's say you defined your interfaces and types in a separate file, and you want to use them in another place. The interface will hide the shape of the object when you hover over it, and the type will not. Look how it looks like in VS code.
Behavior in VS code
You can read the shape of the interface. You just need to click on the interface and you will be moved to its definition. In VS code it ctrl + left mouse button.
Impact on build/compile time
Under this repo you have official recommendations on when to use types and when to use interfaces. The authors of this document provide arguments connected with performance.
By saying performance I mean:
- how long build process take,
- how fast your real-time plugins work in IDE,
- how long the compilation process take.
In old versions of TS there was a small impact with enum, but in modern TS you can avoid this.
I will verify this impact by changing interfaces to types in my big repository in which I have almost 500 interfaces. Later, I'll measure the build/compilation and developer experience in real time (I'm working with VS code).
So I'll execute following commands to compile and build:
tsc --project tsconfig.base.json --diagnostics - compilation.
npx nx run-many --target=build --all - build.
The --diagnostics flag will force compiler to prompt results.
Both commands will be executed on 2 separate branches. First with interfaces and second with types.
Under this repository you have code before changes (with interfaces).
Under this repository you have code after changes (with types).
This is how I made a changes:
The change from interfaces to types process
The result is really interesting... Looks like it's really improving the build time and compile time.
Build time with interfaces
Build time with types
Compile time with interfaces
Compile time with types
I repeated these tests many times. In every attempt I cleaned the build folders and cache. In the context of numbers, there was always a difference, higher or lower, but the conclusion is: the version with interfaces had always faster build and compilation time.
In addition, I tried real-time development on these branches. There were some situations in which when I accessed a file the first time I got a small lag or a longer response from the local TypeScript check server. So, looks like there is an impact too.
If you are curious how it looks on your computer just open above mentioned branches and run tests 🔝.
Should you use types or interfaces?
We spotted a lot of differences between declaring definitions with types and interfaces. Some additional features are available in both of them, but the most important aspect is still the question from the beginning: "Should I use type or interfaces?"
I think the answer is really simple to this question. If TypeScript creators recommend using interfaces for defining objects, we should trust them. In addition, we performed some checks so we know how big the impact is on large projects. It may be several seconds on each build!
So, the real answer is: "Use interfaces first for defining objects until you need a feature from type-based definitions" - of course if you care about development experience and compile/build performance.