DEV Community

Egor Avakumov
Egor Avakumov

Posted on

Keeping TypeScript Interfaces and Implementations in Sync

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;
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
  };
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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) 
    });
  };
}
Enter fullscreen mode Exit fullscreen mode

Installation

Getting started is straightforward:

npm install eslint-plugin-interface-method-style --save-dev
Enter fullscreen mode Exit fullscreen mode

Then add it to your ESLint config:

import interfaceMethodStyle from "eslint-plugin-interface-method-style";

export default [
  interfaceMethodStyle.configs.recommended,
];
Enter fullscreen mode Exit fullscreen mode

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)