TL;DR: eslint-plugin-interface-method-style гарантирует, что ваши TypeScript реализации соответствуют стилю, определенному в интерфейсах. Если интерфейс объявляет метод (method(): void), реализация должна быть методом. Если объявлено свойство-функция (method: () => void), нужна стрелочная функция. Это предотвращает баги с правилом unbound-method и делает код предсказуемым.


Если вы работали с большими TypeScript проектами, то наверняка замечали, как легко накапливаются мелкие несоответствия в коде. Сегодня хочу рассказать о небольшом, но полезном ESLint плагине, который помогает поддерживать консистентность между интерфейсами и их реализациями.

Проблема

Представьте, что у вас есть такой интерфейс:

interface UserService {
  getUser(): User;
  validateUser: (user: User) => boolean;
}

Обратите внимание: getUser определен как сигнатура метода, а validateUser — как свойство, содержащее функцию. Это два разных стиля, и оба имеют право на жизнь. Иногда нужна гибкость стрелочных функций (для привязки this), иногда удобнее обычные методы.

Загвоздка возникает при реализации интерфейса. Очень легко случайно перепутать стили:

class UserServiceImpl implements UserService {
  getUser = () => {  // Упс! В интерфейсе это метод
    return new User();
  };
  
  validateUser(user: User) {  // Упс! В интерфейсе это стрелочная функция
    return user.isValid();
  }
}

TypeScript не будет жаловаться — обе реализации технически корректны. Но такая несогласованность может запутать, особенно когда другие разработчики ожидают, что реализация будет соответствовать стилю интерфейса.

Решение: eslint-plugin-interface-method-style

Этот плагин делает одну вещь, и делает её хорошо: следит, чтобы реализации соблюдали стиль, определенный в интерфейсах. Если интерфейс определяет метод — реализуйте метод. Если стрелочную функцию — реализуйте стрелочную функцию.

Вот как это работает:

interface UserService {
  getUser(): User;              // Сигнатура метода
  validateUser: (user: User) => boolean;  // Сигнатура свойства
}

class UserServiceImpl implements UserService {
  getUser() {                   // ✅ Соответствует интерфейсу
    return new User();
  }
  
  validateUser = (user: User) => {  // ✅ Соответствует интерфейсу
    return user.isValid();
  };
}

Почему это важно

Читаемость: Глядя на интерфейс, вы сразу понимаете, как будут выглядеть его реализации. Никаких сюрпризов.

Сохранение намерений: Автор интерфейса выбрал определенный стиль не случайно — возможно, ему нужны стрелочные функции для правильной привязки this в React компонентах, или методы для более чистого синтаксиса. Плагин гарантирует, что это намерение сохранится.

Код-ревью: На одну вещь меньше нужно следить при ревью кода. Линтер отловит такие несоответствия автоматически.

Критично: совместимость с правилом unbound-method: Вот менее очевидная, но важная причина. Если вы используете правило @typescript-eslint/unbound-method (которое предупреждает о небезопасном использовании методов), несоответствия между интерфейсом и реализацией могут привести к тому, что это правило будет молчать:

interface ILogger {
  log(): void;  // Интерфейс говорит: метод
}

class Logger implements ILogger {
  value = 'test';
  
  log = () => {  // Реализация: стрелочная функция
    console.log(this.value);
  }
}

const logger = new Logger();
const fn = logger.log;  // ⚠️ unbound-method не предупредит!
someFunction(fn);  // Работает, но правило слепо к потенциальным проблемам

Правило unbound-method смотрит на реализацию (стрелочная функция), а не на контракт интерфейса (метод). Когда они не совпадают, вы теряете гарантии безопасности, которые должно обеспечивать unbound-method. Поддерживая синхронизацию интерфейсов и реализаций, вы гарантируете, что проверки безопасности ESLint работают как задумано.

Гибкая конфигурация

Плагин предлагает две опции конфигурации, которые делают его адаптивным к разным требованиям проекта:

Переопределение через prefer

Возможно, вы хотите стандартизировать один стиль во всей кодовой базе, независимо от того, что говорит интерфейс. Это легко сделать:

// Заставить всё быть методами
{
  "rules": {
    "interface-method-style/interface-method-style": [
      "error",
      { "prefer": "method" }
    ]
  }
}

Теперь даже если интерфейс определяет стрелочные функции, реализация должна использовать методы. Это отлично подходит для команд, которые хотят единый стиль повсюду.

Контроль статических членов через ignoreStatic

По умолчанию плагин не проверяет статические члены (так как они не приходят из интерфейсов). Но если вы хотите консистентности и там, можно включить проверку:

{
  "rules": {
    "interface-method-style/interface-method-style": [
      "error",
      { "ignoreStatic": false }
    ]
  }
}

Практический пример

Вот реальный пример с API клиентом:

type ApiClient = {
  get(url: string): Promise<Response>;
  post: (url: string, data: any) => Promise<Response>;
};

class HttpClient implements ApiClient {
  get(url: string) {
    // Синтаксис метода - чисто и просто
    return fetch(url);
  }
  
  post = async (url: string, data: any) => {
    // Стрелочная функция - возможно, нужна правильная привязка this
    return fetch(url, { 
      method: "POST", 
      body: JSON.stringify(data) 
    });
  };
}

Установка

Начать работу просто:

npm install eslint-plugin-interface-method-style --save-dev

Затем добавьте в конфигурацию ESLint:

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

export default [
  interfaceMethodStyle.configs.recommended,
];

Заключение

Это не революционный плагин, который перевернёт вашу кодовую базу. Скорее, это полезный помощник, который поддерживает порядок. В маленьких проектах вы можете не заметить большой разницы. Но в крупных кодовых базах с несколькими разработчиками такие мелкие проверки консистентности складываются в более поддерживаемый код.

Плагин учитывает и особые случаи: перегрузки функций всегда требуют синтаксиса методов (потому что так работает TypeScript), абстрактные методы обрабатываются корректно. Видно, что автор продумал реальные сценарии использования.

Если вы работаете над TypeScript проектом и хотите поддерживать лучшую консистентность между интерфейсами и реализациями, попробуйте. Это небольшое дополнение к вашей настройке линтера, которое может избавить вас от нескольких озадачивающих моментов в будущем.

Посмотреть можно на GitHub.