eslinttypescript-eslint

SyntaxError: Syntax error in selector when using (:not) and (:has)


The error message:

SyntaxError: Syntax error in selector ":not(TSTypeAliasDeclaration:has(> Identifier[name=Nullable])):has(> TSUnionType:has(> TSNullKeyword):not(:has(> TSUndefinedKeyword)))" at position 32: Expected " ", "!", "#", "*", ".", ":", ":first-child", ":has(", ":last-child", ":matches(", ":not(", ":nth-child(", ":nth-last-child(", "[", or [^ [\],():#!=><~+.] but ">" found.

package.json (reduced):

{
    "devDependencies": {
        "eslint": "^9.26.0",
        "typescript": "^5.5.4",
        "typescript-eslint": "^8.33.1",
    },
}

.eslintrc:

{
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./tsconfig.json"
  },
  "rules": {
    "no-restricted-syntax": [
      "error",
      {
        "selector": ":not(TSTypeAliasDeclaration:has(> Identifier[name=Nullable])):has(> TSUnionType:has( > TSNullKeyword):not(:has(> TSUndefinedKeyword)))",
        "message": "Use `Nullable<T>` utility type instead of `| null`."
      },
      {
        "selector": ":not(TSTypeAliasDeclaration:has(> Identifier[name=Undefinable])):has(> TSUnionType:has( > TSUndefinedKeyword):not(:has(> TSNullKeyword)))",
        "message": "Use `Undefinable<T>` utility type instead of `| undefined`."
      },
      {
        "selector": "TSUnionType:has(> TSNullKeyword):has(> TSUndefinedKeyword)",
        "message": "Use `Nilable<T>` utility type instead of `| null | undefined`."
      }
    ]
  }
}

tsconfig.json:

{
    "include": ["src"]
}

test.ts:

type Nullable<T> = T | null;
type Undefinable<T> = T | undefined;
type Nilable<T> = Nullable<Undefinable<T>>;

type x1 = number | null;
type x2 = number | undefined;
type x3 = number | null | undefined;

type x4 = null | number;
type x5 = undefined | number;
type x6 = null | number | undefined;

My goal is to have strict rule to use:

I can achieve it in playground but not in my project


Solution

  • Looking at the message, it seems the parser does not like selectors starting with > inside the pseudo element :has . While this should be fine as per specification, we can work around it. Also, the workaround i'm suggesting removes nested :has which is not allowed by specifications.

    https://drafts.csswg.org/selectors-4/#has-pseudo

    The :has() pseudo-class cannot be nested; :has() is not valid within :has().

    {
      "rules": {
            "no-restricted-syntax": [
              "error",
    
              {
                "selector": ":not(TSTypeAliasDeclaration:has(Identifier[name=Nullable])) > TSUnionType:has(TSNullKeyword):not(:has(TSUndefinedKeyword))",
                "message": "Use `Nullable<T>` utility type instead of `| null`."
              },
              {
                "selector": ":not(TSTypeAliasDeclaration:has(Identifier[name=Undefinable])) > TSUnionType:has(TSUndefinedKeyword):not(:has(TSNullKeyword))",
                "message": "Use `Undefinable<T>` utility type instead of `| undefined`."
              },
              {
                "selector": "TSUnionType:has( TSNullKeyword):has( TSUndefinedKeyword)",
                "message": "Use `Nilable<T>` utility type instead of `| null | undefined`."
              }
            ]
      }
    }
    

    Please note that this will not target TypeAliasDeclaration anymore, but the UnionType instead. From my point of view, this is not so bad as it is the UnionType that is wrong, not the full declaration, but that may not be what you'd like. I tried to find changes so that the whole declaration would be targeted but I couldn't find the right pattern to use.