Interface vs. Type: Why I wrote my own ESLint rule (and how it changed our team)
As a front-end developer with more than six years of experience, I have been through plenty of TypeScript debates. One that always starts a passionate argument is the classic dilemma of “Interface vs. Type”. Early in my career I did not think about it much. Both seemed to do the job. But as projects grew and teams got bigger, I realised that small decisions like this can make the codebase consistent, or break that consistency. That is why I eventually wrote my own ESLint rule, to enforce type aliases instead of interface declarations in our TypeScript codebase. In this article I will share why I decided to do it, how I built the rule, and how it changed the way we work. I will also throw in a few tips so you can try it yourself.
Chaos when a team scales without clear rules
When I started working with TypeScript at my company, the team was small, just a handful of developers. We had a verbal agreement to stick to type aliases instead of interface declarations, because they felt more predictable (more on that later). At first it worked beautifully. We were aligned, the code was clean, and life was fine. Then the team grew fast. Within a year we had 10 developers contributing to the same codebase, and that is when things got complicated.
New teammates were not always aware of our “unwritten rule”. Some preferred interface, because it felt more familiar from their Java background. Others mixed both approaches without even noticing. Code reviews turned into endless debates about style rather than substance. It was clear we needed a better way to enforce consistency, something automated, reliable, and able to scale. So I decided to roll up my sleeves and build my own ESLint plugin: eslint-plugin-interface-to-type. It would automatically enforce type instead of interface across the whole codebase and save us from ourselves.
Why I chose type over interface
Before we get into the technical details, let us talk about why I chose type over interface in the first place. At a glance they look interchangeable. Both describe the shape of an object, right? But there is a subtle difference that can bite you on a larger project: declaration merging.
In TypeScript, if you declare two interface definitions with the same name, they are merged into one automatically. Here is a quick example:
interface User {
id: number;
}
interface User {
name: string;
}
// TypeScript merges them into:
interface User {
id: number;
name: string;
}
That behaviour can be useful in some cases, for example when extending types from third-party libraries. In a collaborative codebase it is a recipe for confusion. Picture two developers unknowingly declaring conflicting properties in separate files. That is a bug waiting to happen. Type aliases, on the other hand, do not merge. If you try to declare two type aliases with the same name, TypeScript throws a compile error, which forces you to resolve the conflict up front. For me, that predictability is a lifesaver.
Besides declaration merging, type aliases are more flexible. They can represent unions, intersections, and primitives, things that interface struggles with. Interface still has its place (for example when you need to extend a class), but I found that type is the safer default for most of our use cases.
Building the ESLint rule: From chaos to consistency
Once I had a clear reason to enforce type, I needed a tool that would do it automatically. That is where my ESLint plugin, eslint-plugin-interface-to-type, comes in. I wanted a rule that would flag any interface declaration, suggest replacing it with a type alias, and even offer an autofix to speed the process up.
Writing a custom ESLint rule was a bit scary at first. I had never written one. After some research I got the hang of it. The plugin uses the ESLint AST (Abstract Syntax Tree) to detect interface declarations and transform them into equivalent type aliases. For example, this:
interface User {
id: number;
name: string;
role: 'admin' | 'user';
}
gets flagged, and it can be fixed automatically to:
type User = {
id: number;
name: string;
role: 'admin' | 'user';
};
The hardest part was the edge cases, such as interface declarations that extend other interfaces, or ones with complex generics. I spent hours tuning the rule so it stayed compatible and avoided false positives. Another challenge was the autofix. The ESLint --fix option needs exact transformations, so I had to map interface syntax onto type syntax carefully, without breaking the code.
Once the plugin was done, setting it up was straightforward. Install it as a dev dependency:
npm install eslint-plugin-interface-to-type --save-dev
Then add it to your ESLint config:
{
"plugins": ["interface-to-type"],
"rules": {
"interface-to-type/prefer-type-over-interface": "error"
}
}
Run ESLint with --fix and it converts your interface declarations into type aliases. Done.
Impact on how we work: Less arguing, more building
The effect on the team was immediate. We used to spend chunks of code review arguing about interface vs. type. Now the rule enforces consistency for us and frees up mental space for discussions that matter more, such as architecture or performance.
It also saved us time. New developers no longer needed a long introductory lecture about our style preferences. The rule simply handled it. Code reviews got faster, because we were no longer nitpicking syntax. It is a small change, but the knock-on effect was huge.
Tips for developers: How to make this work for your team
If the idea appeals to you, here are a few tips to help you enforce type, or any coding standard, in your projects:
- Understand your needs first. Before you enforce type instead of interface, make sure it matches what your team is trying to do. If you rely heavily on declaration merging or on class implementations, interface may still have a place. For us, type worked better in 99% of cases, so it made sense to standardise on it.
- Automate it with tools such as ESLint. Do not rely on verbal agreements. They do not scale. Tools such as ESLint (or Prettier for formatting) can enforce rules consistently across the whole team. Writing your own rule can sound intimidating, but it is a great thing to learn, and it pays off in the long run.
- Get the team on board. If you are introducing a new standard, get feedback from your colleagues first. Before I rolled the ESLint rule out, I shared it with the team, and their input helped me catch a few edge cases I had missed.
Try it yourself and let me know
Switching to type aliases and enforcing them with a custom ESLint rule was a real change for my team. The code became more predictable, reviews more productive, and onboarding smoother. If you are dealing with similar problems, or you just want to experiment with stricter standards, I recommend trying it.
Start by installing eslint-plugin-interface-to-type and running it on your codebase. See what it feels like to have a consistent approach to types in TypeScript. Do you see interface vs. type differently? Or did the rule surface unexpected problems in your project?