TL;DR: eslint-plugin-interface-method-style ensures your TypeScript implementations match the style defined in interfaces. If an interface declares a method (method(): void
), your implementation must be a method. If it declares a property function (method: () => void
), you must use an arrow function. This prevents bugs with unbound-method
rule and keeps code predictable.
If you've worked on a large TypeScript codebase, you've probably noticed how easy it is for inconsistencies to creep in. Today, I want to share a small but useful ESLint plugin that helps maintain consistency between your interfaces and their implementations.
The Problem
Let's say you have an interface like this:
interface UserService {
getUser(): User;
validateUser: (user: User) => boolean;
}
Notice how getUser
is defined as a method signature, while validateUser
is a property that holds a function? These are two different styles, and they exist for good reasons - sometimes you want the flexibility of arrow functions (for binding this
), and sometimes you prefer traditional methods.
The tricky part comes when implementing this interface. It's easy to accidentally mix things up:
class UserServiceImpl implements UserService {
getUser = () => { // Oops! Interface says method
return new User();
};
validateUser(user: User) { // Oops! Interface says arrow function
return user.isValid();
}
}
TypeScript won't complain about this - both implementations are technically valid. But this inconsistency can lead to confusion, especially when other developers expect the implementation to match the interface style.
Enter eslint-plugin-interface-method-style
This plugin does one thing well: it ensures your implementations respect the style defined in your interfaces. If your interface defines something as a method, you implement it as a method. If it's an arrow function property, you implement it as an arrow function property.
Here's how it works:
interface UserService {
getUser(): User; // Method signature
validateUser: (user: User) => boolean; // Property signature
}
class UserServiceImpl implements UserService {
getUser() { // ✅ Matches interface
return new User();
}
validateUser = (user: User) => { // ✅ Matches interface
return user.isValid();
};
}
Why This Matters
Readability: When you look at an interface, you immediately know how its implementations will look. No surprises.
Intent preservation: The interface author chose a specific style for a reason - maybe they wanted arrow functions for proper this
binding in React components, or methods for cleaner syntax. The plugin ensures that intent is preserved.
Code reviews: One less thing to watch out for during code reviews. The linter catches these inconsistencies automatically.
Critical: Compatibility with unbound-method
rule: Here's a less obvious but important reason. If you use the @typescript-eslint/unbound-method
rule (which warns about unsafe method usage), mismatches between interface and implementation can cause this rule to fail silently:
interface ILogger {
log(): void; // Interface says: method
}
class Logger implements ILogger {
value = 'test';
log = () => { // Implementation: arrow function
console.log(this.value);
}
}
const logger = new Logger();
const fn = logger.log; // ⚠️ unbound-method won't warn!
someFunction(fn); // Works, but the rule is blind to potential issues
The unbound-method
rule looks at the implementation (arrow function) rather than the interface contract (method). When they don't match, you lose the safety guarantees that unbound-method
is supposed to provide. By keeping interfaces and implementations in sync, you ensure that ESLint's safety checks work as intended.
Flexible Configuration
The plugin offers two configuration options that make it adaptable to different project needs:
Override with prefer
Maybe you want to standardize on one style across your entire codebase, regardless of what the interface says. You can do that:
// Force everything to be methods
{
"rules": {
"interface-method-style/interface-method-style": [
"error",
{ "prefer": "method" }
]
}
}
Now even if your interface defines arrow functions, your implementation must use methods. This is great for teams that want a single, consistent style everywhere.
Control static members with ignoreStatic
By default, the plugin doesn't check static members (because they don't come from interfaces). But if you want consistency there too, you can enable checking:
{
"rules": {
"interface-method-style/interface-method-style": [
"error",
{ "ignoreStatic": false }
]
}
}
Real-World Example
Here's a practical example with an API client:
type ApiClient = {
get(url: string): Promise<Response>;
post: (url: string, data: any) => Promise<Response>;
};
class HttpClient implements ApiClient {
get(url: string) {
// Method syntax - clean and simple
return fetch(url);
}
post = async (url: string, data: any) => {
// Arrow function - maybe we need proper 'this' binding
return fetch(url, {
method: "POST",
body: JSON.stringify(data)
});
};
}
Installation
Getting started is straightforward:
npm install eslint-plugin-interface-method-style --save-dev
Then add it to your ESLint config:
import interfaceMethodStyle from "eslint-plugin-interface-method-style";
export default [
interfaceMethodStyle.configs.recommended,
];
Final Thoughts
This isn't a game-changing plugin that will revolutionize your codebase. It's more like a helpful assistant that keeps things tidy. In small projects, you might not notice much difference. But in larger codebases with multiple developers, these small consistency checks add up to create more maintainable code.
The plugin respects special cases too - function overloads always require method syntax (because that's how TypeScript works), and abstract methods are handled correctly. It's clear the author thought about real-world usage scenarios.
If you're working on a TypeScript project and want to maintain better consistency between your interfaces and implementations, give it a try. It's a small addition to your linting setup that might save you from some head-scratching moments down the road.
Check it out on GitHub.
Top comments (0)