As a front-end developer with over six years of experience, I’ve seen my fair share of TypeScript debates. One that always sparks heated discussions is the classic “Interface vs. Type” dilemma. Early in my career, I didn’t think much of it — both seemed to get the job done. But as projects grew and teams expanded, I realized that small decisions like this can make or break code consistency. That’s why I ended up creating a custom ESLint rule to enforce using type
aliases over interface
declarations in our TypeScript codebase. In this article, I’ll share why I made that choice, how I built the rule, and how it transformed our workflow. Plus, I’ll throw in a few tips for you to try it out yourself.
The Chaos of Scaling a Team Without Clear Rules
When I started working with TypeScript at my company, our team was small — just a handful of developers. We had a verbal agreement to stick with type
aliases instead of interface
declarations because we found them more predictable (more on that later). It worked great at first. We were aligned, our code was clean, and life was good. But then our team grew fast. Within a year, we had 10 developers contributing to the same codebase, and that’s when things got messy.
New team members weren’t 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 scalable. So, I decided to roll up my sleeves and create a custom ESLint plugin: eslint-plugin-interface-to-type. It would automatically enforce the use of type
over interface
across our entire codebase, saving us from ourselves.
Why I Chose type
Over interface
Before diving into the technical details, let’s talk about why I picked type
over interface
in the first place. At first glance, they might seem interchangeable — they both define shapes for objects, right? But there’s a subtle difference that can bite you in larger projects: Declaration Merging.
In TypeScript, if you declare two interface
definitions with the same name, they automatically merge into one. Here’s a quick example:
interface User {
id: number;
}
interface User {
name: string;
}
// TypeScript merges them into:
interface User {
id: number;
name: string;
}
This behavior can be useful in some cases, like extending third-party library types. But in a collaborative environment, it’s a recipe for confusion. Imagine two developers unknowingly declaring conflicting properties in separate files — it’s a bug waiting to happen. On the other hand, type
aliases don’t merge. If you try to declare two type
aliases with the same name, TypeScript throws a compilation error, forcing you to resolve the conflict upfront. For me, that predictability is a lifesaver.
Beyond Declaration Merging, type
aliases are more flexible. They can represent unions, intersections, and primitives — things interface
struggles with. While interface
has its place (like when you need to extend classes), I found type
to be the safer default for most of our use cases.
Building the ESLint Rule: From Chaos to Consistency
Now that I had a clear reason to enforce type
, I needed a tool to make it happen automatically. That’s 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 provide an autofix option to streamline the process.
Building a custom ESLint rule was a bit daunting at first — I’d never written one before. But after some research, I got the hang of it. The plugin uses ESLint’s 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 can be autofixed to:
type User = {
id: number;
name: string;
role: 'admin' | 'user';
};
The hardest part was handling edge cases — like interface
declarations that extend other interfaces or have complex generics. I spent hours tweaking the rule to ensure compatibility and avoid false positives. Another challenge was implementing the autofix feature. ESLint’s — fix
option requires you to provide precise transformations, so I had to carefully map interface
syntax to type
syntax without breaking the code.
Once the plugin was ready, setting it up was straightforward. You just 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’ll automatically convert your interface
declarations to type
aliases. Done!
Impact on Our Workflow: Less Fighting, More Building
The impact of this rule on our team was immediate. Before, we’d spend chunks of code reviews arguing over interface
vs. type
. Now, the rule enforces consistency for us, freeing up mental space for more important discussions — like architecture or performance optimizations.
It also saved us time. New developers no longer needed a lengthy onboarding lecture about our style preferences. The rule just handled it. Code reviews became faster because we weren’t nitpicking over syntax anymore. It’s a small change, but the ripple effects were huge.
Tips for Developers: How to Make This Work for Your Team
If you’re intrigued by this idea, here are a few tips to help you enforce type usage — or any coding standard — in your projects:
- Understand Your Needs First: Before enforcing
type
overinterface
, make sure it fits your team’s goals. If you rely heavily on Declaration Merging or class implementations,interface
might still have a place. For us,type
worked better 99% of the time, so it made sense to standardize on it. - Automate with Tools Like ESLint: Don’t rely on verbal agreements — they don’t scale. Tools like ESLint (or Prettier for formatting) can enforce rules consistently across a team. Writing a custom rule might sound intimidating, but it’s a great learning experience and pays off in the long run.
- Encourage Buy-In from Your Team: If you’re introducing a new standard, get feedback from your colleagues first. I shared my ESLint rule with the team before rolling it out, and their input helped me catch a few edge cases I’d missed.
Try It Yourself and Let Me Know!
Switching to type
aliases and enforcing it with a custom ESLint rule was a game-changer for my team. It made our code more predictable, our reviews more productive, and our onboarding smoother. If you’re dealing with similar issues — or just want to experiment with stricter standards — I’d encourage you to give it a shot.
Start by installing eslint-plugin-interface-to-type and running it on your codebase. See how it feels to have a consistent approach to TypeScript types. Got a different perspective on interface
vs. type
? Or did the rule uncover some unexpected issues in your project? Drop a comment below — I’d love to hear your thoughts!
Top comments (0)