---
id: custom-rules
sidebar_label: Building Custom Rules
title: Custom Rules
---

:::important
This page describes how to write your own custom ESLint rules using typescript-eslint.
You should be familiar with [ESLint's developer guide](https://eslint.org/docs/developer-guide) and [ASTs](/blog/asts-and-typescript-eslint) before writing custom rules.
:::

As long as you are using `@typescript-eslint/parser` as the `parser` in your ESLint configuration, custom ESLint rules generally work the same way for JavaScript and TypeScript code.
The main four changes to custom rules writing are:

- [Utils Package](#utils-package): we recommend using `@typescript-eslint/utils` to create custom rules
- [AST Extensions](#ast-extensions): targeting TypeScript-specific syntax in your rule selectors
- [Typed Rules](#typed-rules): using the TypeScript type checker to inform rule logic
- [Testing](#testing): using `@typescript-eslint/rule-tester`'s `RuleTester` instead of ESLint core's

## Utils Package

The [`@typescript-eslint/utils`](../packages/Utils.mdx) package acts as a replacement package for `eslint` that exports all the same objects and types, but with typescript-eslint support.
It also exports common utility functions and constants most custom typescript-eslint rules tend to use.

:::caution
`@types/eslint` types are based on `@types/estree` and do not recognize typescript-eslint nodes and properties.
You should generally not need to import from `eslint` when writing custom typescript-eslint rules in TypeScript.
:::

### `RuleCreator`

The recommended way to create custom ESLint rules that make use of typescript-eslint features and/or syntax is with the `ESLintUtils.RuleCreator` function exported by `@typescript-eslint/utils`.

It takes in a function that transforms a rule name into its documentation URL, then returns a function that takes in a rule module object.
`RuleCreator` will infer the allowed message IDs the rule is allowed to emit from the provided `meta.messages` object.

This rule bans function declarations that start with a lower-case letter:

```ts
import { ESLintUtils } from '@typescript-eslint/utils';

const createRule = ESLintUtils.RuleCreator(
  name => `https://example.com/rule/${name}`,
);

// Type: RuleModule<"uppercase", ...>
export const rule = createRule({
  create(context) {
    return {
      FunctionDeclaration(node) {
        if (node.id != null) {
          if (/^[a-z]/.test(node.id.name)) {
            context.report({
              messageId: 'uppercase',
              node: node.id,
            });
          }
        }
      },
    };
  },
  name: 'uppercase-first-declarations',
  meta: {
    docs: {
      description:
        'Function declaration names should start with an upper-case letter.',
    },
    messages: {
      uppercase: 'Start this name with an upper-case letter.',
    },
    type: 'suggestion',
    schema: [],
  },
  defaultOptions: [],
});
```

`RuleCreator` rule creator functions return rules typed as the `RuleModule` interface exported by `@typescript-eslint/utils`.
It allows specifying generics for:

- `MessageIds`: a union of string literal message IDs that may be reported
- `Options`: what options users may configure for the rule (by default, `[]`)

If the rule is able to take in rule options, declare them as a tuple type containing a single object of rule options:

```ts
import { ESLintUtils } from '@typescript-eslint/utils';

type MessageIds = 'lowercase' | 'uppercase';

type Options = [
  {
    preferredCase?: 'lower' | 'upper';
  },
];

// Type: RuleModule<MessageIds, Options, ...>
export const rule = createRule<Options, MessageIds>({
  // ...
});
```

#### Extra Rule Docs Types

By default, rule `meta.docs` is allowed to contain only `description` and `url` as described in [ESLint's Custom Rules > Rule Structure docs](https://eslint.org/docs/latest/extend/custom-rules#rule-structure).
Additional docs properties may be added as a type argument to `ESLintUtils.RuleCreator`:

```ts
interface MyPluginDocs {
  recommended: boolean;
}

const createRule = ESLintUtils.RuleCreator<MyPluginDocs>(
  name => `https://example.com/rule/${name}`,
);

createRule({
  // ...
  meta: {
    docs: {
      description: '...',
      recommended: true,
    },
    // ...
  },
});
```

### Undocumented Rules

Although it is generally not recommended to create custom rules without documentation, if you are sure you want to do this you can use the `ESLintUtils.RuleCreator.withoutDocs` function to directly create a rule.
It applies the same type inference as the `createRule`s above without enforcing a documentation URL.

```ts
import { ESLintUtils } from '@typescript-eslint/utils';

export const rule = ESLintUtils.RuleCreator.withoutDocs({
  create(context) {
    // ...
  },
  meta: {
    // ...
  },
});
```

:::caution
We recommend any custom ESLint rule include a descriptive error message and link to informative documentation.
:::

## Handling rule options

ESLint rules can take options. When handling options, you will need to add information in at most three places:

- The `Options` generic type argument to `RuleCreator`, where you declare the type of the options
- The `meta.schema` property, where you add a JSON schema describing the options shape
- The `defaultOptions` property, where you add the default options value

```ts
type MessageIds = 'lowercase' | 'uppercase';

type Options = [
  {
    preferredCase: 'lower' | 'upper';
  },
];

export const rule = createRule<Options, MessageIds>({
  meta: {
    // ...
    schema: [
      {
        type: 'object',
        properties: {
          preferredCase: {
            type: 'string',
            enum: ['lower', 'upper'],
          },
        },
        additionalProperties: false,
      },
    ],
  },
  defaultOptions: [
    {
      preferredCase: 'lower',
    },
  ],
  create(context, options) {
    if (options[0].preferredCase === 'lower') {
      // ...
    }
  },
});
```

:::warning

When reading the options, use the second parameter of the `create` function, not `context.options` from the first parameter. The first is created by ESLint and does not have the default options applied.

:::

## AST Extensions

`@typescript-eslint/estree` creates AST nodes for TypeScript syntax with names that begin with `TS`, such as `TSInterfaceDeclaration` and `TSTypeAnnotation`.
These nodes are treated just like any other AST node.
You can query for them in your rule selectors.

This version of the above rule instead bans interface declaration names that start with a lower-case letter:

```ts
import { ESLintUtils } from '@typescript-eslint/utils';

export const rule = createRule({
  create(context) {
    return {
      TSInterfaceDeclaration(node) {
        if (/^[a-z]/.test(node.id.name)) {
          // ...
        }
      },
    };
  },
  // ...
});
```

### Node Types

TypeScript types for nodes exist in a `TSESTree` namespace exported by `@typescript-eslint/utils`.
The above rule body could be better written in TypeScript with a type annotation on the `node`:

An `AST_NODE_TYPES` enum is exported as well to hold the values for AST node `type` properties.
`TSESTree.Node` is available as union type that uses its `type` member as a discriminant.

For example, checking `node.type` can narrow down the type of the `node`:

```ts
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';

export function describeNode(node: TSESTree.Node): string {
  switch (node.type) {
    case AST_NODE_TYPES.ArrayExpression:
      return `Array containing ${node.elements.map(describeNode).join(', ')}`;

    case AST_NODE_TYPES.Literal:
      return `Literal value ${node.raw}`;

    default:
      return '🤷';
  }
}
```

### Explicit Node Types

Rule queries that use more features of [esquery](https://github.com/estools/esquery) such as targeting multiple node types may not be able to infer the type of the `node`.
In that case, it is best to add an explicit type declaration.

This rule snippet targets name nodes of both function and interface declarations:

```ts
import { TSESTree } from '@typescript-eslint/utils';

export const rule = createRule({
  create(context) {
    return {
      'FunctionDeclaration, TSInterfaceDeclaration'(
        node: TSESTree.FunctionDeclaration | TSESTree.TSInterfaceDeclaration,
      ) {
        if (/^[a-z]/.test(node.id.name)) {
          // ...
        }
      },
    };
  },
  // ...
});
```

## Typed Rules

:::tip
Read TypeScript's [Compiler APIs > Type Checker APIs](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#type-checker-apis) for how to use a program's type checker.
:::

The biggest addition typescript-eslint brings to ESLint rules is the ability to use TypeScript's type checker APIs.

`@typescript-eslint/utils` exports an `ESLintUtils` namespace containing a `getParserServices` function that takes in an ESLint context and returns a `services` object.

That `services` object contains:

- `program`: A full TypeScript `ts.Program` object if type checking is enabled, or `null` otherwise
- `esTreeNodeToTSNodeMap`: Map of `@typescript-eslint/estree` `TSESTree.Node` nodes to their TypeScript `ts.Node` equivalents
- `tsNodeToESTreeNodeMap`: Map of TypeScript `ts.Node` nodes to their `@typescript-eslint/estree` `TSESTree.Node` equivalents

If type checking is enabled, that `services` object additionally contains:

- `getTypeAtLocation`: Wraps the type checker function, with a `TSESTree.Node` parameter instead of a `ts.Node`
- `getSymbolAtLocation`: Wraps the type checker function, with a `TSESTree.Node` parameter instead of a `ts.Node`

Those additional objects internally map from ESTree nodes to their TypeScript equivalents, then call to the TypeScript program.
By using the TypeScript program from the parser services, rules are able to ask TypeScript for full type information on those nodes.

This rule bans for-of looping over an enum by using the TypeScript type checker via typescript-eslint's services:

```ts
import { ESLintUtils } from '@typescript-eslint/utils';
import * as ts from 'typescript';

export const rule = createRule({
  create(context) {
    return {
      ForOfStatement(node) {
        // 1. Grab the parser services for the rule
        const services = ESLintUtils.getParserServices(context);

        // 2. Find the TS type for the ES node
        const type = services.getTypeAtLocation(node.right);

        // 3. Check the TS type's backing symbol for being an enum
        if (type.symbol.flags & ts.SymbolFlags.Enum) {
          context.report({
            messageId: 'loopOverEnum',
            node: node.right,
          });
        }
      },
    };
  },
  meta: {
    docs: {
      description: 'Avoid looping over enums.',
    },
    messages: {
      loopOverEnum: 'Do not loop over enums.',
    },
    type: 'suggestion',
    schema: [],
  },
  name: 'no-loop-over-enum',
  defaultOptions: [],
});
```

:::note
Rules can retrieve their full backing TypeScript type checker with `services.program.getTypeChecker()`.
This can be necessary for TypeScript APIs not wrapped by the parser services.
:::

### Conditional Type Information

We recommend _against_ changing rule logic based solely on whether `services.program` exists.
In our experience, users are generally surprised when rules behave differently with or without type information.
Additionally, if they misconfigure their ESLint config, they may not realize why the rule started behaving differently.
Consider either gating type checking behind an explicit option for the rule or creating two versions of the rule instead.

:::tip
Documentation generators such as [`eslint-doc-generator`](https://github.com/bmish/eslint-doc-generator) can automatically indicate in a rule's docs whether it needs type information.
:::

## Testing

`@typescript-eslint/rule-tester` exports a `RuleTester` with a similar API to the built-in ESLint `RuleTester`.
It should be provided with the same `parser` and `parserOptions` you would use in your ESLint configuration.

Below is a quick-start guide. For more in-depth docs and examples [see the `@typescript-eslint/rule-tester` package documentation](../packages/Rule_Tester.mdx).

### Testing Untyped Rules

For rules that don't need type information, no constructor parameters are necessary:

```ts
import { RuleTester } from '@typescript-eslint/rule-tester';
import rule from './my-rule';

const ruleTester = new RuleTester();

ruleTester.run('my-rule', rule, {
  valid: [
    /* ... */
  ],
  invalid: [
    /* ... */
  ],
});
```

### Testing Typed Rules

For rules that do need type information, `parserOptions` must be passed in as well.
We recommend using `parserOptions.projectService` with options to allow a default project for each test file.

```ts
import { RuleTester } from '@typescript-eslint/rule-tester';
import rule from './my-typed-rule';

const ruleTester = new RuleTester({
  languageOptions: {
    parserOptions: {
      projectService: {
        allowDefaultProject: ['*.ts*'],
      },
      tsconfigRootDir: __dirname,
    },
  },
});

ruleTester.run('my-typed-rule', rule, {
  valid: [
    /* ... */
  ],
  invalid: [
    /* ... */
  ],
});
```

See [_Rule Tester_ > _Type-Aware Testing_](../packages/Rule_Tester.mdx#type-aware-testing) for more details.
