🐧

typescript-eslint で理解する Visitor パターン

に公開

この記事は「Medley (メドレー) Advent Calendar 2025」の8日目の記事となります。

はじめに

こんにちは、メドレーのエンジニアの吉岡です。
社内の有志で輪読会を行っており、最近だと「Java言語で学ぶデザインパターン入門 第3版」を読みました。

https://www.hyuki.com/dp/

その中で Visitor パターンについて学んだので、その知識を元に typescript-eslint の不具合修正に挑戦してみた話を書こうと思います。

Visitor パターンとは

Visitor パターンとは、GoF のデザインパターンの一つで、「データ構造と処理を分離する」 ためのパターンです。

通常、オブジェクト指向ではデータと処理をクラス内にまとめますが、Visitor パターンではこれらを分けます。

  • データ構造(Element):
    固定的な構造(例: ディレクトリ、ファイル、ASTノード)
  • 訪問者(Visitor):
    データ構造を渡り歩いて処理を行う人(例: サイズ計算、出力、Lintチェック)

これにより、「データ構造のクラスを変更することなく、新しい処理を自由に追加できる」 というメリットがあります。

輪読会時点では、処理が複雑になり実際のプロダクト開発で使うことがあるのだろうかというのが正直な感想でした。

typescript-eslint における Visitor パターン

私の所属しているチームの CLINICS では、フロントエンドは React、TypeScript で開発を行っており、リンターとして、ESLinttypescript-eslint を使用しています。
typescript-eslint では Visitor パターンを採用することで、ルールの追加を容易にし、高い拡張性を実現しています。

具体的には、Visitor パターンの構成要素が以下のようになります。

  • データ構造(Element):
    ソースコードを解析して生成された AST(抽象構文木)
  • 訪問者(Visitor):
    我々が作成する Lint ルール

わかりやすい例として、any 型を禁止する no-explicit-any の実装を見てみましょう。AST 上で anyTSAnyKeyword というノードで表現されます。

https://github.com/typescript-eslint/typescript-eslint/blob/8fe34456f75c1d1e8a4dc518306d5ab93422efec/packages/eslint-plugin/src/rules/no-explicit-any.ts#L1-L250

export default createRule({
  name: 'no-explicit-any',
  // ...(省略)
  
  create(context) {
    return {
      // "TSAnyKeyword" という種類のノードを訪問した時だけ、この関数が呼ばれる
      TSAnyKeyword(node) {
        
        // (...省略...)

        // エラーを報告する
        context.report({
          node,
          messageId: 'unexpectedAny',
          // ...省略
        });
      },
    };
  },
});

上記のように、AST 側のコードを変更することなく、新しい Lint ルールを簡単に追加できるようになっています。

typescript-eslint の不具合

typescript-eslint の Issues を眺めていたところ、以下の Issue を見つけました。

https://github.com/typescript-eslint/typescript-eslint/issues/11771

no-unused-private-class-members という、クラスのプライベートなメンバ変数に未使用なものがないかをチェックするルールに関するものでした。不具合の内容は、プライベートなメンバ変数を以下のように this から分割代入した場合に、使用しているにも関わらず「未使用」と判定されてしまうというものです。

class A {
  private readonly a = 1;
  private readonly b = 2;


  call() {
    // ここで使っているはずなのに、未使用と怒られてしまう
    const { a, b } = this;

    console.log(a, b);
  }
}

PR を出してみた

原因を調査するために no-unused-private-class-members の実装を確認しました。

https://github.com/typescript-eslint/typescript-eslint/blob/8fe34456f75c1d1e8a4dc518306d5ab93422efec/packages/eslint-plugin/src/util/class-scope-analyzer/classScopeAnalyzer.ts#L1-L716

従来のコードでは、this.a のような通常のプロパティアクセスについては、以下の MemberExpression で正しく検知できていました。

  // メンバーアクセス式(this.a など)に来たときの処理
  protected MemberExpression(node: TSESTree.MemberExpression): void {
    this.visitChildren(node);

    (省略)

    // 参照されたと判定
    countReference(node, member);
  }

しかし、不具合のあった const { a, b } = this という構文は、AST 上では MemberExpression ではなく VariableDeclarator という別のノードになります。

この VariableDeclarator に対応するチェック処理が存在していなかったため、スルーされてしまっていたのが不具合の原因でした。

そこで、VariableDeclarator を訪問した際にも this からの分割代入をチェックする以下の処理を追加する PR を作成しました(本記事執筆時点ではレビュー中です)。

  // 変数の宣言(const { a } = this など)に来たときの処理
  protected VariableDeclarator(node: TSESTree.VariableDeclarator): void {
    this.visitChildren(node);

    // 右辺が 'this' かつ、左辺がオブジェクトパターン(分割代入)の場合
    if (
      node.init?.type === AST_NODE_TYPES.ThisExpression &&
      node.id.type === AST_NODE_TYPES.ObjectPattern
    ) {
      // 使用済みと判定する処理
      this.handleThisDestructuring(node.id);
    }
  }

このように、「新しいノード のチェックを増やしたい場合は、対応するメソッドを Visitor に追加するだけ」という実装になっており、Visitor パターンの「機能拡張の容易さ」を実感することができました。

まとめ

  • Visitor パターンは、データ構造と処理を分離することで、機能拡張を容易に行うことができる
  • 普段使用している typescript-eslint の例を見ることで Visitor パターンに対する理解を深めることができた

終わりに

メドレーでは様々な職種で人材を募集しているので、興味がある方はぜひお声かけください。

Medley Advent Calendar 2025、明日は @yoko_masa さんです!

株式会社メドレー

Discussion