🚥

Value Objectの===を防ぐeslintルール

2024/09/21に公開

なんの記事?

Value Object(以下VO)をtypescriptでclassとして実装するとき、値の比較にはisEqual,equalsのようなメソッドを作成して用いることが一般的です。

class VO {
  private readonly _value: string;

  constructor(value: string) {
    this._value = value;
  }

  get value(): string {
    return this._value;
  }
  isEqual(input: VO) {
    // よしなに実装する
    return this.value === input.value;
  }
}

これにより、比較のロジックを閉じ込めたうえで、カスタマイズできるようになっています。
このとき、valueを取得して直接演算することは先述のメリットを台無しにするので避けたいですよね。

// こうしてほしい
console.log(new VO("a").isEqual(new VO("a")));
// これは許したくない
console.log(new VO("a").value === new VO("a").value);

ルールとして決めてもよいのですが、書けるものはいつか書いてしまうので、linterかコンパイラに怒ってもらう方法を考えたいです。

eslintでvalueの===を禁止する

eslintno-restricted-syntaxというルールを用います。
これは特定のsyntaxを禁止するもので、次のような設定をすることでvalueとvalueを===で結ぶことを禁止できます。

// eslint.config.js
export default [
  {
    rules: {
      "no-restricted-syntax": [
        "error",
        {
          selector:
            "BinaryExpression[operator='==='][left.property.name='value'][right.property.name='value']",
          message: "厳密等価演算子ではなくiseEqualメソッドを使ってね",
        },
      ],
    },
  },
];

先程のコードを見てみるとeslintからのエラーを確認できるようになりました。

// エラーが出る
// 厳密等価演算子ではなくequalメソッドを使ってねESLintno-restricted-syntax

console.log(new VO("a").value === new VO("a").value);
// OK
console.log(new VO("a").isEqual(new VO("a")));

シンプルな実装で目的を達成できたのですが、課題が2つあります。

  • 他の場所でもvalueの厳密等価比較ができない
  • とくにisEqual内部で比較する場所にもエラーが出る

syntaxとして弾いてしまっているので、VO classのここもエラーになってしまうわけです。

  isEqual(input: VO) {
    // 厳密等価演算子ではなくequalメソッドを使ってねESLintno-restricted-syntax
    return this.value === input.value;
  }

エラーを回避

eslintの抑制コメントを用いる

回避策としてはeslintを抑制するコメントを用いるというのがあります。

  isEqual(input: VO) {
    // エラーが出なくなる
    // eslint-disable-next-line no-restricted-syntax
    return this.value === input.value;
  }
  isEqual(input: VO) {
    // エラーが出なくなる
    // @ts-expect-error
    return this.value === input.value;
  }

これによりエラーは出なくなります。
多様は控えるべきですが、説明できる場所でこれらのコメントを利用することは落とし所としてはあるかと思います。

適応するファイルを制限する

たとえば、domain/models/以下のファイルではルールを無効にする、などが考えられます。

// eslint.config.js
export default [
  {
    rules: {
      "no-restricted-syntax": [
        "error",
        {
          selector:
            "BinaryExpression[operator='==='][left.property.name='value'][right.property.name='value']",
          message: "厳密等価演算子ではなくiseEqualメソッドを使ってね",
        },
      ],
    },
  },
   overrides: [
    {
      // domain/models ディレクトリ以下のファイルに対するルール
      files: ['**/domain/models/**/*.ts'],
      rules: {
        'no-restricted-syntax': 'off', // domain/modelsディレクトリでは無効化
      },
    },
];

ルールを厳格にする

現在のルール設定ではすべてのvalueプロパティ比較を禁じていますが、例えば

  • ValueObject classを継承したインスタンス同士の場合のみvalueプロパティ同士の===を禁止する

などが実現できればいいですよね。
eslintはスクリプトでルールを追加できるので、たとえば次のようなルールを作れば実現できるようです。
(AI生成のコード)

// custom-no-value-comparison.js
module.exports = {
  meta: {
    type: "problem",
    docs: {
      description:
        "Disallow direct comparison of 'value' property for specific class instances",
      category: "Best Practices",
      recommended: false,
    },
    fixable: null,
    schema: [
      {
        type: "array",
        items: { type: "string" },
        uniqueItems: true,
      },
    ],
  },

  create(context) {
    const classNames = context.options[0] || ["MyValueObject"];

    return {
      BinaryExpression(node) {
        if (node.operator !== "===") return;

        const isValueComparison = (expr) =>
          expr.type === "MemberExpression" &&
          expr.property.name === "value" &&
          expr.object.type === "Identifier" &&
          classNames.includes(
            context
              .getScope()
              .variables.find((v) => v.name === expr.object.name)?.defs[0]?.node
              .init.callee.name,
          );

        if (isValueComparison(node.left) && isValueComparison(node.right)) {
          context.report({
            node,
            message: `Direct comparison of 'value' property is not allowed for instances of ${classNames.join(
              ", ",
            )}. Use the equals() method instead.`,
          });
        }
      },
    };
  },
};

ただし、複雑なルールにすると、ルール自体のテストが必要になってくる、という課題もあります。

NCDCエンジニアブログ

Discussion